@synapsor/runner 1.6.0 → 1.6.1

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 (55) hide show
  1. package/CHANGELOG.md +32 -4
  2. package/README.md +16 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/local-ui.d.ts +4 -1
  5. package/dist/local-ui.d.ts.map +1 -1
  6. package/dist/runner.mjs +1624 -42
  7. package/dist/runtime.mjs +1011 -25
  8. package/dist/shadow.mjs +408 -15
  9. package/docs/app-owned-executors.md +8 -0
  10. package/docs/capability-authoring.md +45 -0
  11. package/docs/current-scope.md +5 -0
  12. package/docs/cursor-plugin.md +2 -2
  13. package/docs/limitations.md +15 -2
  14. package/docs/local-mode.md +22 -3
  15. package/docs/production.md +19 -0
  16. package/docs/proposal-evidence-freshness.md +334 -0
  17. package/docs/release-notes.md +30 -4
  18. package/docs/runner-config-reference.md +46 -0
  19. package/docs/security-boundary.md +17 -0
  20. package/docs/store-lifecycle.md +8 -0
  21. package/docs/troubleshooting-first-run.md +76 -0
  22. package/docs/writeback-executors.md +8 -0
  23. package/examples/app-owned-writeback/command-handler.mjs +0 -0
  24. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
  25. package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
  26. package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
  27. package/examples/support-billing-agent/scripts/run-evaluation.sh +0 -0
  28. package/fixtures/compatibility/published-1.6.0/manifest.json +76 -0
  29. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
  30. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
  31. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
  32. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
  33. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
  34. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
  35. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
  36. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
  37. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
  38. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
  39. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
  40. package/fixtures/protocol/MANIFEST.json +37 -7
  41. package/fixtures/protocol/change-set.freshness-update.v2.json +58 -0
  42. package/fixtures/protocol/freshness-authority.invoice.v1.json +35 -0
  43. package/fixtures/protocol/freshness-proof.fresh.v1.json +38 -0
  44. package/fixtures/protocol/writeback-job.freshness-update.v2.json +50 -0
  45. package/llms.txt +1 -0
  46. package/package.json +9 -8
  47. package/schemas/change-set.v1.schema.json +1 -0
  48. package/schemas/change-set.v2.schema.json +1 -0
  49. package/schemas/change-set.v3.schema.json +1 -0
  50. package/schemas/freshness-authority.v1.schema.json +89 -0
  51. package/schemas/freshness-proof.v1.schema.json +73 -0
  52. package/schemas/synapsor.runner.schema.json +32 -0
  53. package/schemas/writeback-job.v1.schema.json +1 -0
  54. package/schemas/writeback-job.v2.schema.json +1 -0
  55. package/schemas/writeback-job.v3.schema.json +1 -0
package/dist/runner.mjs CHANGED
@@ -25064,6 +25064,8 @@ var protocolVersions = {
25064
25064
  runnerProposal: "synapsor.runner-proposal.v1",
25065
25065
  runnerActivity: "synapsor.runner-activity.v1",
25066
25066
  principalScope: "synapsor.principal-scope.v1",
25067
+ freshnessAuthority: "synapsor.freshness-authority.v1",
25068
+ freshnessProof: "synapsor.freshness-proof.v1",
25067
25069
  legacyWritebackJob: "1.0",
25068
25070
  normalizedWritebackJobV2: "2.0",
25069
25071
  normalizedWritebackJobV3: "3.0",
@@ -25111,6 +25113,101 @@ var principalScopeGuardSchema = z.object({
25111
25113
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope fingerprint does not match its trusted value", path: ["value_fingerprint"] });
25112
25114
  }
25113
25115
  });
25116
+ var freshnessDependencyUnsignedSchema = z.object({
25117
+ id: safeIdentifier,
25118
+ capability: z.string().regex(/^[A-Za-z_][A-Za-z0-9_.-]*\.[A-Za-z_][A-Za-z0-9_.-]*$/, "expected reviewed capability name"),
25119
+ source_id: z.string().min(1).max(160),
25120
+ engine: writebackEngineSchema,
25121
+ target: z.object({
25122
+ schema: safeIdentifier,
25123
+ table: safeIdentifier,
25124
+ primary_key: columnValueSchema,
25125
+ tenant_column: safeIdentifier,
25126
+ principal_column: safeIdentifier.optional()
25127
+ }),
25128
+ expected_version: columnValueSchema,
25129
+ evidence: z.object({
25130
+ bundle_id: z.string().min(1).max(200),
25131
+ query_fingerprint: sha256
25132
+ })
25133
+ });
25134
+ var freshnessDependencyV1Schema = freshnessDependencyUnsignedSchema.extend({
25135
+ descriptor_digest: sha256
25136
+ }).superRefine((dependency, ctx) => {
25137
+ const { descriptor_digest: _digest, ...unsigned } = dependency;
25138
+ if (canonicalJsonDigest(unsigned) !== dependency.descriptor_digest) {
25139
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency digest mismatch", path: ["descriptor_digest"] });
25140
+ }
25141
+ });
25142
+ var freshnessAuthorityUnsignedSchema = z.object({
25143
+ schema_version: z.literal(protocolVersions.freshnessAuthority),
25144
+ required: z.literal(true),
25145
+ target: z.object({
25146
+ mode: z.enum(["exact_guard", "frozen_set", "not_applicable"]),
25147
+ member_count: z.number().int().min(0).max(100)
25148
+ }),
25149
+ dependencies: z.array(freshnessDependencyV1Schema).max(16)
25150
+ });
25151
+ var freshnessAuthorityV1Schema = freshnessAuthorityUnsignedSchema.extend({
25152
+ dependency_set_digest: sha256
25153
+ }).superRefine((authority, ctx) => {
25154
+ const identities = authority.dependencies.map((dependency) => dependency.id);
25155
+ if (new Set(identities).size !== identities.length) {
25156
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency ids must be unique", path: ["dependencies"] });
25157
+ }
25158
+ const sorted = [...authority.dependencies].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.target.schema.localeCompare(right.target.schema) || left.target.table.localeCompare(right.target.table) || JSON.stringify(left.target.primary_key.value).localeCompare(JSON.stringify(right.target.primary_key.value)) || left.id.localeCompare(right.id));
25159
+ if (JSON.stringify(sorted) !== JSON.stringify(authority.dependencies)) {
25160
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependencies must use deterministic lock order", path: ["dependencies"] });
25161
+ }
25162
+ const { dependency_set_digest: _digest, ...unsigned } = authority;
25163
+ if (canonicalJsonDigest(unsigned) !== authority.dependency_set_digest) {
25164
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency-set digest mismatch", path: ["dependency_set_digest"] });
25165
+ }
25166
+ });
25167
+ var freshnessCheckStatusSchema = z.enum(["fresh", "stale", "unavailable", "invalid", "unsupported"]);
25168
+ var freshnessItemResultSchema = z.object({
25169
+ id: z.string().min(1).max(160),
25170
+ kind: z.enum(["target", "supporting"]),
25171
+ status: z.enum(["fresh", "stale", "not_applicable", "unavailable", "invalid", "unsupported"]),
25172
+ safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
25173
+ expected_version_digest: sha256.optional(),
25174
+ observed_version_digest: sha256.optional()
25175
+ });
25176
+ var freshnessProofUnsignedSchema = z.object({
25177
+ schema_version: z.literal(protocolVersions.freshnessProof),
25178
+ proposal_id: z.string().min(1),
25179
+ proposal_hash: sha256,
25180
+ proposal_version: z.number().int().positive(),
25181
+ dependency_set_digest: sha256,
25182
+ checked_at: z.string().datetime(),
25183
+ valid_until: z.string().datetime(),
25184
+ source_adapters: z.array(z.object({
25185
+ source_id: z.string().min(1).max(160),
25186
+ engine: writebackEngineSchema
25187
+ })).min(1).max(8),
25188
+ result: freshnessCheckStatusSchema,
25189
+ safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
25190
+ target_count: z.number().int().min(0).max(100),
25191
+ supporting_count: z.number().int().min(0).max(16),
25192
+ checks: z.array(freshnessItemResultSchema).max(116)
25193
+ });
25194
+ var freshnessProofV1Schema = freshnessProofUnsignedSchema.extend({
25195
+ proof_digest: sha256
25196
+ }).superRefine((proof, ctx) => {
25197
+ if (Date.parse(proof.valid_until) < Date.parse(proof.checked_at)) {
25198
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof validity ends before it was checked", path: ["valid_until"] });
25199
+ }
25200
+ if (proof.checks.filter((check) => check.kind === "target").length !== proof.target_count) {
25201
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness target count mismatch", path: ["target_count"] });
25202
+ }
25203
+ if (proof.checks.filter((check) => check.kind === "supporting").length !== proof.supporting_count) {
25204
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness supporting count mismatch", path: ["supporting_count"] });
25205
+ }
25206
+ const { proof_digest: _digest, ...unsigned } = proof;
25207
+ if (canonicalJsonDigest(unsigned) !== proof.proof_digest) {
25208
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof digest mismatch", path: ["proof_digest"] });
25209
+ }
25210
+ });
25114
25211
  var resolvedDeduplicationComponentSchema = z.object({
25115
25212
  column: safeIdentifier,
25116
25213
  value: scalar,
@@ -25240,6 +25337,7 @@ var changeSetV1Schema = z.object({
25240
25337
  allowed_columns: z.array(z.string().min(1)).min(1),
25241
25338
  expected_version: columnValueSchema
25242
25339
  }),
25340
+ freshness: freshnessAuthorityV1Schema.optional(),
25243
25341
  evidence: z.object({
25244
25342
  bundle_id: z.string().min(1),
25245
25343
  query_fingerprint: sha256,
@@ -25260,6 +25358,7 @@ var changeSetV1Schema = z.object({
25260
25358
  }),
25261
25359
  created_at: z.string().min(1)
25262
25360
  }).superRefine((changeSet, ctx) => {
25361
+ validateFreshnessBinding(changeSet.freshness, changeSet.source.source_id, sourceEngine(changeSet.source.kind), "exact_guard", 1, ctx);
25263
25362
  const allowed = new Set(changeSet.guards.allowed_columns);
25264
25363
  for (const column of Object.keys(changeSet.patch)) {
25265
25364
  if (!allowed.has(column)) {
@@ -25327,6 +25426,7 @@ var changeSetV2Schema = z.object({
25327
25426
  version_advance: versionAdvanceSchema.optional(),
25328
25427
  deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional()
25329
25428
  }),
25429
+ freshness: freshnessAuthorityV1Schema.optional(),
25330
25430
  reversibility: reversibilityRequestSchema.optional(),
25331
25431
  evidence: z.object({
25332
25432
  bundle_id: z.string().min(1),
@@ -25346,6 +25446,14 @@ var changeSetV2Schema = z.object({
25346
25446
  integrity: z.object({ proposal_hash: sha256 }),
25347
25447
  created_at: z.string().min(1)
25348
25448
  }).superRefine((changeSet, ctx) => {
25449
+ validateFreshnessBinding(
25450
+ changeSet.freshness,
25451
+ changeSet.source.source_id,
25452
+ sourceEngine(changeSet.source.kind),
25453
+ changeSet.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
25454
+ changeSet.operation === "single_row_insert" ? 0 : 1,
25455
+ ctx
25456
+ );
25349
25457
  const allowed = new Set(changeSet.guards.allowed_columns);
25350
25458
  for (const column of Object.keys(changeSet.patch)) {
25351
25459
  if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
@@ -25412,6 +25520,7 @@ var changeSetV3Schema = z.object({
25412
25520
  expected_version: columnValueSchema.optional(),
25413
25521
  version_advance: versionAdvanceSchema.optional()
25414
25522
  }),
25523
+ freshness: freshnessAuthorityV1Schema.optional(),
25415
25524
  frozen_set: frozenSetSchema,
25416
25525
  reversibility: reversibilityRequestSchema.optional(),
25417
25526
  evidence: z.object({ bundle_id: z.string().min(1), query_fingerprint: sha256, items: z.array(z.unknown()).max(100) }).passthrough(),
@@ -25426,6 +25535,14 @@ var changeSetV3Schema = z.object({
25426
25535
  integrity: z.object({ proposal_hash: sha256 }),
25427
25536
  created_at: z.string().min(1)
25428
25537
  }).superRefine((changeSet, ctx) => {
25538
+ validateFreshnessBinding(
25539
+ changeSet.freshness,
25540
+ changeSet.source.source_id,
25541
+ sourceEngine(changeSet.source.kind),
25542
+ changeSet.operation === "batch_insert" ? "not_applicable" : "frozen_set",
25543
+ changeSet.operation === "batch_insert" ? 0 : changeSet.frozen_set.row_count,
25544
+ ctx
25545
+ );
25429
25546
  if (changeSet.source.primary_key.value !== void 0 || changeSet.guards.expected_version) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set guards live on frozen members, not the top-level envelope", path: ["frozen_set"] });
25430
25547
  const allowed = new Set(changeSet.guards.allowed_columns);
25431
25548
  for (const column of Object.keys(changeSet.patch)) if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
@@ -25521,6 +25638,7 @@ var writebackJobV1Schema = z.object({
25521
25638
  allowed_columns: z.array(safeIdentifier).min(1),
25522
25639
  patch: scalarMap,
25523
25640
  conflict_guard: publicConflictGuardSchema,
25641
+ freshness: freshnessAuthorityV1Schema.optional(),
25524
25642
  idempotency_key: z.string().min(1),
25525
25643
  lease: z.object({
25526
25644
  lease_id: z.string().min(1),
@@ -25528,6 +25646,7 @@ var writebackJobV1Schema = z.object({
25528
25646
  expires_at: z.string().min(1)
25529
25647
  })
25530
25648
  }).superRefine((job, ctx) => {
25649
+ validateFreshnessBinding(job.freshness, job.runner_scope.source_id, job.engine, "exact_guard", 1, ctx);
25531
25650
  validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.tenant_guard.column, ctx);
25532
25651
  if (job.principal_scope && job.allowed_columns.includes(job.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
25533
25652
  });
@@ -25549,6 +25668,7 @@ var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
25549
25668
  allowed_columns: job.allowed_columns,
25550
25669
  patch: job.patch,
25551
25670
  conflict_guard: normalizeConflictGuard(job.conflict_guard),
25671
+ ...job.freshness ? { freshness: job.freshness } : {},
25552
25672
  idempotency_key: job.idempotency_key,
25553
25673
  lease_expires_at: job.lease.expires_at,
25554
25674
  attempt_count: job.lease.attempt
@@ -25582,10 +25702,12 @@ var legacyWritebackJobSchema = z.object({
25582
25702
  z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
25583
25703
  z.object({ kind: z.literal("none") })
25584
25704
  ]),
25705
+ freshness: freshnessAuthorityV1Schema.optional(),
25585
25706
  idempotency_key: z.string().min(1),
25586
25707
  lease_expires_at: z.union([z.string(), z.number()]),
25587
25708
  attempt_count: z.number().int().nonnegative().optional()
25588
25709
  }).superRefine((job, ctx) => {
25710
+ validateFreshnessBinding(job.freshness, job.source_id, job.engine, "exact_guard", 1, ctx);
25589
25711
  validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
25590
25712
  if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
25591
25713
  });
@@ -25612,6 +25734,7 @@ var normalizedWritebackJobV2InputSchema = z.object({
25612
25734
  z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
25613
25735
  z.object({ kind: z.literal("none") })
25614
25736
  ]),
25737
+ freshness: freshnessAuthorityV1Schema.optional(),
25615
25738
  version_advance: versionAdvanceSchema.optional(),
25616
25739
  deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional(),
25617
25740
  idempotency_key: z.string().min(1),
@@ -25619,6 +25742,14 @@ var normalizedWritebackJobV2InputSchema = z.object({
25619
25742
  attempt_count: z.number().int().positive(),
25620
25743
  inverse_capture: inverseDescriptorV1Schema.optional()
25621
25744
  }).superRefine((job, ctx) => {
25745
+ validateFreshnessBinding(
25746
+ job.freshness,
25747
+ job.source_id,
25748
+ job.engine,
25749
+ job.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
25750
+ job.operation === "single_row_insert" ? 0 : 1,
25751
+ ctx
25752
+ );
25622
25753
  if (job.operation !== "single_row_insert" && job.target.primary_key.value === void 0) {
25623
25754
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
25624
25755
  }
@@ -25684,6 +25815,7 @@ var writebackJobV2Schema = z.object({
25684
25815
  principal_scope: principalScopeGuardSchema.optional(),
25685
25816
  allowed_columns: z.array(safeIdentifier),
25686
25817
  mutation: writebackMutationV2Schema,
25818
+ freshness: freshnessAuthorityV1Schema.optional(),
25687
25819
  idempotency_key: z.string().min(1),
25688
25820
  inverse_capture: inverseDescriptorV1Schema.optional(),
25689
25821
  lease: z.object({
@@ -25692,6 +25824,14 @@ var writebackJobV2Schema = z.object({
25692
25824
  expires_at: z.string().min(1)
25693
25825
  })
25694
25826
  }).superRefine((job, ctx) => {
25827
+ validateFreshnessBinding(
25828
+ job.freshness,
25829
+ job.runner_scope.source_id,
25830
+ job.engine,
25831
+ job.mutation.kind === "single_row_insert" ? "not_applicable" : "exact_guard",
25832
+ job.mutation.kind === "single_row_insert" ? 0 : 1,
25833
+ ctx
25834
+ );
25695
25835
  const mutation = job.mutation;
25696
25836
  if (mutation.kind !== "single_row_insert" && job.target.primary_key.value === void 0) {
25697
25837
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
@@ -25730,6 +25870,7 @@ var writebackJobV2Schema = z.object({
25730
25870
  conflict_guard: job.mutation.kind === "single_row_insert" ? { kind: "none" } : normalizeConflictGuard(job.mutation.conflict_guard),
25731
25871
  ...job.mutation.kind === "single_row_update" && job.mutation.version_advance ? { version_advance: job.mutation.version_advance } : {},
25732
25872
  ...job.mutation.kind === "single_row_insert" ? { deduplication: job.mutation.deduplication } : {},
25873
+ ...job.freshness ? { freshness: job.freshness } : {},
25733
25874
  idempotency_key: job.idempotency_key,
25734
25875
  ...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
25735
25876
  lease_expires_at: job.lease.expires_at,
@@ -25748,6 +25889,7 @@ var normalizedWritebackJobV3InputSchema = z.object({
25748
25889
  allowed_columns: z.array(safeIdentifier).max(256),
25749
25890
  patch: boundedScalarRecord,
25750
25891
  conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
25892
+ freshness: freshnessAuthorityV1Schema.optional(),
25751
25893
  version_advance: versionAdvanceSchema.optional(),
25752
25894
  frozen_set: frozenSetSchema,
25753
25895
  idempotency_key: z.string().min(1),
@@ -25755,6 +25897,14 @@ var normalizedWritebackJobV3InputSchema = z.object({
25755
25897
  attempt_count: z.number().int().positive(),
25756
25898
  inverse_capture: inverseDescriptorV1Schema.optional()
25757
25899
  }).superRefine((job, ctx) => {
25900
+ validateFreshnessBinding(
25901
+ job.freshness,
25902
+ job.source_id,
25903
+ job.engine,
25904
+ job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
25905
+ job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
25906
+ ctx
25907
+ );
25758
25908
  if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
25759
25909
  if (job.operation === "set_delete" && (job.allowed_columns.length || Object.keys(job.patch).length || job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set DELETE cannot carry patch authority", path: ["patch"] });
25760
25910
  if (job.operation === "set_update" && (!Object.keys(job.patch).length || !job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set UPDATE requires patch and version advance", path: ["patch"] });
@@ -25776,9 +25926,19 @@ var writebackJobV3Schema = z.object({
25776
25926
  patch: boundedScalarRecord,
25777
25927
  version_advance: versionAdvanceSchema.optional(),
25778
25928
  frozen_set: frozenSetSchema,
25929
+ freshness: freshnessAuthorityV1Schema.optional(),
25779
25930
  idempotency_key: z.string().min(1),
25780
25931
  inverse_capture: inverseDescriptorV1Schema.optional(),
25781
25932
  lease: z.object({ lease_id: z.string().min(1), attempt: z.number().int().positive(), expires_at: z.string().min(1) })
25933
+ }).superRefine((job, ctx) => {
25934
+ validateFreshnessBinding(
25935
+ job.freshness,
25936
+ job.runner_scope.source_id,
25937
+ job.engine,
25938
+ job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
25939
+ job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
25940
+ ctx
25941
+ );
25782
25942
  }).transform((job) => ({
25783
25943
  protocol_version: protocolVersions.normalizedWritebackJobV3,
25784
25944
  job_id: job.writeback_job_id,
@@ -25793,6 +25953,7 @@ var writebackJobV3Schema = z.object({
25793
25953
  conflict_guard: { kind: "none" },
25794
25954
  ...job.version_advance ? { version_advance: job.version_advance } : {},
25795
25955
  frozen_set: job.frozen_set,
25956
+ ...job.freshness ? { freshness: job.freshness } : {},
25796
25957
  idempotency_key: job.idempotency_key,
25797
25958
  ...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
25798
25959
  lease_expires_at: job.lease.expires_at,
@@ -26146,6 +26307,38 @@ var runnerActivityV1Schema = z.object({
26146
26307
  detail: z.record(z.unknown()).optional(),
26147
26308
  occurred_at: z.string().min(1).max(80).optional()
26148
26309
  }).strict();
26310
+ function sourceEngine(kind) {
26311
+ if (kind === "external_postgres") return "postgres";
26312
+ if (kind === "external_mysql") return "mysql";
26313
+ return void 0;
26314
+ }
26315
+ function validateFreshnessBinding(freshness, sourceId, engine, expectedTargetMode, expectedMemberCount, ctx) {
26316
+ if (!freshness) return;
26317
+ if (!engine) {
26318
+ ctx.addIssue({
26319
+ code: z.ZodIssueCode.custom,
26320
+ message: "freshness authority requires an external PostgreSQL or MySQL source",
26321
+ path: ["freshness"]
26322
+ });
26323
+ return;
26324
+ }
26325
+ if (freshness.target.mode !== expectedTargetMode || freshness.target.member_count !== expectedMemberCount) {
26326
+ ctx.addIssue({
26327
+ code: z.ZodIssueCode.custom,
26328
+ message: `freshness target authority must be ${expectedTargetMode} with ${expectedMemberCount} reviewed member(s)`,
26329
+ path: ["freshness", "target"]
26330
+ });
26331
+ }
26332
+ for (const [index, dependency] of freshness.dependencies.entries()) {
26333
+ if (dependency.source_id !== sourceId || dependency.engine !== engine) {
26334
+ ctx.addIssue({
26335
+ code: z.ZodIssueCode.custom,
26336
+ message: "freshness dependencies must use the proposal/writeback source and engine",
26337
+ path: ["freshness", "dependencies", index, "source_id"]
26338
+ });
26339
+ }
26340
+ }
26341
+ }
26149
26342
  function validateAllowedPatchColumns(allowedColumns, patchColumns, primaryKeyColumn, tenantGuardColumn, ctx) {
26150
26343
  const allow = new Set(allowedColumns);
26151
26344
  for (const column of patchColumns) {
@@ -26206,6 +26399,12 @@ function normalizeConflictGuard(guard) {
26206
26399
  function parseChangeSet(input) {
26207
26400
  return z.union([changeSetV1Schema, changeSetV2Schema, changeSetV3Schema, compensationChangeSetV1Schema]).parse(input);
26208
26401
  }
26402
+ function parseFreshnessAuthority(input) {
26403
+ return freshnessAuthorityV1Schema.parse(input);
26404
+ }
26405
+ function parseFreshnessProof(input) {
26406
+ return freshnessProofV1Schema.parse(input);
26407
+ }
26209
26408
  function parseWritebackJob(input) {
26210
26409
  return writebackJobSchema.parse(input);
26211
26410
  }
@@ -26576,10 +26775,12 @@ function sleep(ms) {
26576
26775
 
26577
26776
  // packages/config/src/index.ts
26578
26777
  init_dist();
26579
- var TOP_LEVEL_KEYS2 = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "generated_authority", "strict", "result_format"]);
26778
+ var TOP_LEVEL_KEYS2 = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "proposal_freshness", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "generated_authority", "strict", "result_format"]);
26580
26779
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
26581
26780
  var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
26582
26781
  var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
26782
+ var PROPOSAL_FRESHNESS_KEYS = /* @__PURE__ */ new Set(["approval", "dependencies"]);
26783
+ var FRESHNESS_DEPENDENCY_KEYS = /* @__PURE__ */ new Set(["id", "capability", "identity_from_arg", "version_column"]);
26583
26784
  var METRICS_KEYS = /* @__PURE__ */ new Set(["enabled", "token_env"]);
26584
26785
  var GRADUATED_TRUST_KEYS = /* @__PURE__ */ new Set(["enabled", "kill_switch", "workspace_id", "project_id", "criteria"]);
26585
26786
  var GRADUATED_TRUST_CRITERION_KEYS = /* @__PURE__ */ new Set([
@@ -26824,6 +27025,7 @@ function validateRunnerCapabilityConfig(input) {
26824
27025
  validateMetrics(input.metrics, strict, errors);
26825
27026
  validateGraduatedTrust(input.graduated_trust, strict, errors);
26826
27027
  validateCapabilities2(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings, hasContracts);
27028
+ validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
26827
27029
  validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
26828
27030
  validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
26829
27031
  validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
@@ -27911,6 +28113,125 @@ function validateCapabilities2(value, sources, contexts, executors, mode, strict
27911
28113
  capabilityNames.set(capability.name, index);
27912
28114
  });
27913
28115
  }
28116
+ function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
28117
+ if (value === void 0) return;
28118
+ if (!isRecord2(value)) {
28119
+ errors.push({
28120
+ path: "$.proposal_freshness",
28121
+ code: "PROPOSAL_FRESHNESS_NOT_OBJECT",
28122
+ message: "proposal_freshness must map reviewed proposal capability names to freshness policy."
28123
+ });
28124
+ return;
28125
+ }
28126
+ const capabilities = Array.isArray(capabilitiesValue) ? capabilitiesValue.filter(isRecord2) : [];
28127
+ const byName = new Map(capabilities.flatMap((capability) => isQualifiedName2(capability.name) ? [[String(capability.name), capability]] : []));
28128
+ for (const [proposalName, rawPolicy] of Object.entries(value)) {
28129
+ const path26 = `$.proposal_freshness.${proposalName}`;
28130
+ if (!isQualifiedName2(proposalName)) {
28131
+ errors.push({ path: path26, code: "INVALID_FRESHNESS_CAPABILITY_NAME", message: "Freshness policy keys must be existing qualified proposal capability names." });
28132
+ continue;
28133
+ }
28134
+ if (!isRecord2(rawPolicy)) {
28135
+ errors.push({ path: path26, code: "FRESHNESS_POLICY_NOT_OBJECT", message: "Freshness policy must be an object." });
28136
+ continue;
28137
+ }
28138
+ if (strict) checkUnknownKeys2(rawPolicy, PROPOSAL_FRESHNESS_KEYS, path26, errors);
28139
+ if (rawPolicy.approval !== "required") {
28140
+ errors.push({ path: `${path26}.approval`, code: "FRESHNESS_APPROVAL_REQUIRED", message: "Freshness-enabled proposals must set approval to required." });
28141
+ }
28142
+ const proposal = byName.get(proposalName);
28143
+ if (!proposal) {
28144
+ errors.push({ path: path26, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
28145
+ continue;
28146
+ }
28147
+ if (proposal.kind !== "proposal") {
28148
+ errors.push({ path: path26, code: "FRESHNESS_PROPOSAL_REQUIRED", message: `${proposalName} must be a proposal capability.` });
28149
+ }
28150
+ const writeback2 = isRecord2(proposal.writeback) ? proposal.writeback : void 0;
28151
+ if (writeback2?.mode !== "direct_sql") {
28152
+ errors.push({
28153
+ path: path26,
28154
+ code: "FRESHNESS_DIRECT_SQL_REQUIRED",
28155
+ message: "Strict proposal freshness currently requires same-database direct_sql writeback; app-owned and cross-source handlers must enforce their own transaction preconditions."
28156
+ });
28157
+ }
28158
+ const operation = isRecord2(proposal.operation) ? proposal.operation : void 0;
28159
+ if ((operation?.kind ?? "update") !== "insert") {
28160
+ const guard = isRecord2(proposal.conflict_guard) ? proposal.conflict_guard : void 0;
28161
+ if (!isSafeIdentifier2(guard?.column)) {
28162
+ errors.push({ path: path26, code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED", message: "Freshness-required UPDATE/DELETE needs an exact conflict_guard.column; weak row hashes are not sufficient." });
28163
+ }
28164
+ }
28165
+ if (rawPolicy.dependencies !== void 0 && !Array.isArray(rawPolicy.dependencies)) {
28166
+ errors.push({ path: `${path26}.dependencies`, code: "FRESHNESS_DEPENDENCIES_NOT_ARRAY", message: "dependencies must be an array of reviewed single-row read references." });
28167
+ continue;
28168
+ }
28169
+ const dependencies = Array.isArray(rawPolicy.dependencies) ? rawPolicy.dependencies : [];
28170
+ if (dependencies.length > 16) {
28171
+ errors.push({ path: `${path26}.dependencies`, code: "FRESHNESS_DEPENDENCY_LIMIT", message: "A proposal may declare at most 16 supporting freshness dependencies." });
28172
+ }
28173
+ const ids = /* @__PURE__ */ new Set();
28174
+ for (const [index, rawDependency] of dependencies.entries()) {
28175
+ const dependencyPath = `${path26}.dependencies[${index}]`;
28176
+ if (!isRecord2(rawDependency)) {
28177
+ errors.push({ path: dependencyPath, code: "FRESHNESS_DEPENDENCY_NOT_OBJECT", message: "Freshness dependency must be an object." });
28178
+ continue;
28179
+ }
28180
+ if (strict) checkUnknownKeys2(rawDependency, FRESHNESS_DEPENDENCY_KEYS, dependencyPath, errors);
28181
+ if (!isSafeIdentifier2(rawDependency.id)) {
28182
+ errors.push({ path: `${dependencyPath}.id`, code: "INVALID_FRESHNESS_DEPENDENCY_ID", message: "Dependency id must be a fixed safe identifier." });
28183
+ } else if (ids.has(String(rawDependency.id))) {
28184
+ errors.push({ path: `${dependencyPath}.id`, code: "DUPLICATE_FRESHNESS_DEPENDENCY_ID", message: "Dependency ids must be unique within a proposal." });
28185
+ } else {
28186
+ ids.add(String(rawDependency.id));
28187
+ }
28188
+ if (!isQualifiedName2(rawDependency.capability)) {
28189
+ errors.push({ path: `${dependencyPath}.capability`, code: "INVALID_FRESHNESS_DEPENDENCY_CAPABILITY", message: "Dependency capability must be a reviewed qualified capability name." });
28190
+ continue;
28191
+ }
28192
+ const dependency = byName.get(String(rawDependency.capability));
28193
+ if (!dependency) {
28194
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
28195
+ continue;
28196
+ }
28197
+ if (dependency.kind !== "read" || dependency.protected_read !== void 0 || dependency.aggregate !== void 0) {
28198
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_SINGLE_ROW_READ_REQUIRED", message: "Supporting freshness dependencies must reference reviewed single-row read capabilities." });
28199
+ }
28200
+ if (dependency.name === proposalName) {
28201
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_CYCLE", message: "A proposal cannot depend on itself." });
28202
+ }
28203
+ if (dependency.source !== proposal.source) {
28204
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_CROSS_SOURCE_UNSUPPORTED", message: "Strict atomic freshness requires the proposal and supporting dependency to use the same source." });
28205
+ }
28206
+ if ((dependency.context ?? null) !== (proposal.context ?? null)) {
28207
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_CONTEXT_MISMATCH", message: "Supporting freshness dependencies must use the same reviewed trusted context as the proposal." });
28208
+ }
28209
+ const target2 = isRecord2(dependency.target) ? dependency.target : void 0;
28210
+ if (!isSafeIdentifier2(target2?.tenant_key) && target2?.single_tenant_dev !== true) {
28211
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_TENANT_SCOPE_REQUIRED", message: "Supporting dependencies require a trusted tenant key or an existing explicit single-tenant-dev boundary." });
28212
+ }
28213
+ const proposalTarget = isRecord2(proposal.target) ? proposal.target : void 0;
28214
+ if (isSafeIdentifier2(target2?.principal_scope_key) && !isSafeIdentifier2(proposalTarget?.principal_scope_key)) {
28215
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_PRINCIPAL_SCOPE_UNAVAILABLE", message: "A principal-scoped dependency cannot introduce a trusted principal value absent from the proposal authority." });
28216
+ }
28217
+ if (!isSafeIdentifier2(rawDependency.identity_from_arg) || !isRecord2(proposal.args) || !(String(rawDependency.identity_from_arg) in proposal.args)) {
28218
+ errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_UNKNOWN", message: "identity_from_arg must name an existing scalar proposal argument." });
28219
+ } else {
28220
+ const proposalArg = isRecord2(proposal.args[String(rawDependency.identity_from_arg)]) ? proposal.args[String(rawDependency.identity_from_arg)] : void 0;
28221
+ if (!proposalArg || proposalArg.type === "object_array") {
28222
+ errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_NOT_SCALAR", message: "identity_from_arg must reference one bounded scalar proposal argument." });
28223
+ }
28224
+ }
28225
+ const lookup = isRecord2(dependency.lookup) ? dependency.lookup : void 0;
28226
+ if (!isSafeIdentifier2(lookup?.id_from_arg) || !isRecord2(dependency.args) || !(String(lookup?.id_from_arg) in dependency.args)) {
28227
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_LOOKUP_INVALID", message: "Supporting capability must have one fixed reviewed lookup argument." });
28228
+ }
28229
+ if (!isSafeIdentifier2(rawDependency.version_column)) {
28230
+ errors.push({ path: `${dependencyPath}.version_column`, code: "FRESHNESS_VERSION_COLUMN_REQUIRED", message: "version_column must be an exact fixed safe identifier." });
28231
+ }
28232
+ }
28233
+ }
28234
+ }
27914
28235
  function validateCapability(value, index, sourceNames, contextNames, executorNames, strict, errors, warnings) {
27915
28236
  const path26 = `$.capabilities[${index}]`;
27916
28237
  if (!isRecord2(value)) {
@@ -30421,6 +30742,14 @@ async function sourceTransaction(client, job, config, fn, hooks = {}) {
30421
30742
  scope: config.databaseScope,
30422
30743
  operations: ["SELECT", rlsOperationForJob(job)]
30423
30744
  });
30745
+ for (const dependency of freshnessDependencies(job)) {
30746
+ await assertPostgresRlsTarget(client, {
30747
+ schema: dependency.target.schema,
30748
+ table: dependency.target.table,
30749
+ scope: config.databaseScope,
30750
+ operations: ["SELECT"]
30751
+ });
30752
+ }
30424
30753
  await bindPostgresRlsScope(client, config.databaseScope);
30425
30754
  }
30426
30755
  await hooks.afterBegin?.();
@@ -30540,6 +30869,15 @@ function validateOperation(job) {
30540
30869
  }
30541
30870
  async function mutatePostgres(job, client) {
30542
30871
  if (job.protocol_version === "4.0") return await mutatePostgresCompensation(job, client);
30872
+ const freshnessConflict = await lockPostgresFreshnessDependencies(job, client);
30873
+ if (freshnessConflict) {
30874
+ return {
30875
+ status: "conflict",
30876
+ affectedRows: 0,
30877
+ code: freshnessConflict,
30878
+ targetIdentity: identityForJob(job)
30879
+ };
30880
+ }
30543
30881
  if (job.protocol_version === "3.0") return await mutatePostgresSet(job, client);
30544
30882
  const operation = operationOf(job);
30545
30883
  if (operation === "single_row_insert") return await insertPostgres(job, client);
@@ -30577,6 +30915,38 @@ async function mutatePostgres(job, client) {
30577
30915
  verifyVersionAdvanced(job, resultVersion);
30578
30916
  return { status: "applied", affectedRows: 1, targetIdentity: identityForJob(job), resultVersion, beforeDigest, afterDigest: digest({ identity: identityForJob(job), patch: job.patch, version: resultVersion }) };
30579
30917
  }
30918
+ function freshnessDependencies(job) {
30919
+ if (!("freshness" in job) || !job.freshness) return [];
30920
+ return job.freshness.dependencies;
30921
+ }
30922
+ async function lockPostgresFreshnessDependencies(job, client) {
30923
+ for (const dependency of freshnessDependencies(job)) {
30924
+ const values = [
30925
+ dependency.target.primary_key.value,
30926
+ job.target.tenant_guard.value
30927
+ ];
30928
+ const where = [
30929
+ `${quotePostgresIdentifier(dependency.target.primary_key.column)} = $1`,
30930
+ `${quotePostgresIdentifier(dependency.target.tenant_column)} = $2`
30931
+ ];
30932
+ if (dependency.target.principal_column) {
30933
+ const scope = principalScope(job);
30934
+ if (!scope) return "FRESHNESS_DEPENDENCY_SCOPE_INVALID";
30935
+ values.push(scope.value);
30936
+ where.push(`${quotePostgresIdentifier(dependency.target.principal_column)} = $${values.length}`);
30937
+ }
30938
+ const result = await client.query(
30939
+ `SELECT ${quotePostgresIdentifier(dependency.expected_version.column)}::text AS "__synapsor_freshness_version" FROM ${quotePostgresIdentifier(dependency.target.schema)}.${quotePostgresIdentifier(dependency.target.table)} WHERE ${where.join(" AND ")} FOR UPDATE`,
30940
+ values
30941
+ );
30942
+ if (result.rowCount !== 1) return "FRESHNESS_DEPENDENCY_STALE";
30943
+ if (!versionValuesMatch(
30944
+ result.rows[0]?.__synapsor_freshness_version,
30945
+ dependency.expected_version.value
30946
+ )) return "FRESHNESS_DEPENDENCY_STALE";
30947
+ }
30948
+ return void 0;
30949
+ }
30580
30950
  function compensationProjection(job) {
30581
30951
  const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
30582
30952
  const scope = principalScope(job);
@@ -31176,6 +31546,15 @@ var PostgresProposalRuntimeStore = class {
31176
31546
  async createProposal(input) {
31177
31547
  return await this.withWrite("createProposal", (store) => store.createProposal(input));
31178
31548
  }
31549
+ async recordFreshnessProof(input) {
31550
+ return await this.withWrite("recordFreshnessProof", (store) => store.recordFreshnessProof(input));
31551
+ }
31552
+ async latestFreshnessProof(proposalId) {
31553
+ return await this.withRead((store) => store.latestFreshnessProof(proposalId));
31554
+ }
31555
+ async recordFreshnessApprovalBlocked(proposalId, input) {
31556
+ await this.withWrite("recordFreshnessApprovalBlocked", (store) => store.recordFreshnessApprovalBlocked(proposalId, input));
31557
+ }
31179
31558
  async approveProposalByPolicy(proposalId, options) {
31180
31559
  return await this.withWrite("approveProposalByPolicy", (store) => store.approveProposalByPolicy(proposalId, options));
31181
31560
  }
@@ -31624,6 +32003,7 @@ var ProposalStore = class {
31624
32003
  decision_hash TEXT,
31625
32004
  signature TEXT,
31626
32005
  integrity_hash TEXT,
32006
+ freshness_proof_digest TEXT,
31627
32007
  created_at TEXT NOT NULL,
31628
32008
  FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
31629
32009
  );
@@ -31897,6 +32277,7 @@ var ProposalStore = class {
31897
32277
  this.ensureColumn("approvals", "decision_hash", "TEXT");
31898
32278
  this.ensureColumn("approvals", "signature", "TEXT");
31899
32279
  this.ensureColumn("approvals", "integrity_hash", "TEXT");
32280
+ this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
31900
32281
  }
31901
32282
  ensureSearchIndexes() {
31902
32283
  this.db.exec(`
@@ -32187,6 +32568,75 @@ var ProposalStore = class {
32187
32568
  const row = this.db.prepare("SELECT proposal_id FROM query_audit WHERE evidence_bundle_id = ? AND proposal_id IS NOT NULL ORDER BY created_at DESC LIMIT 1").get(evidenceBundleId);
32188
32569
  return isRecord4(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
32189
32570
  }
32571
+ recordFreshnessProof(input) {
32572
+ const proof = parseFreshnessProof(input);
32573
+ const proposal = this.requireProposal(proof.proposal_id);
32574
+ assertProposalIdentity(proposal, proof.proposal_hash, proof.proposal_version);
32575
+ const authority = proposalFreshnessAuthority(proposal);
32576
+ if (!authority) {
32577
+ throw new ProposalStoreError(
32578
+ "FRESHNESS_NOT_REQUIRED",
32579
+ `proposal ${proof.proposal_id} has no reviewed freshness authority`
32580
+ );
32581
+ }
32582
+ if (proof.dependency_set_digest !== authority.dependency_set_digest) {
32583
+ throw new ProposalStoreError(
32584
+ "FRESHNESS_PROOF_AUTHORITY_MISMATCH",
32585
+ `freshness proof does not match proposal ${proof.proposal_id}`
32586
+ );
32587
+ }
32588
+ const now = (/* @__PURE__ */ new Date()).toISOString();
32589
+ this.transaction(() => {
32590
+ this.appendEvent(proof.proposal_id, "proposal_freshness_checked", "runner", { proof });
32591
+ if (proof.result === "stale") {
32592
+ const current = this.requireProposal(proof.proposal_id);
32593
+ if (current.state === "pending_review" || current.state === "approved" || current.state === "pending_worker") {
32594
+ this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("conflict", now, proof.proposal_id);
32595
+ this.appendEvent(proof.proposal_id, "proposal_conflict", "runner", {
32596
+ reason: "freshness_stale",
32597
+ safe_code: proof.safe_code,
32598
+ proof_digest: proof.proof_digest
32599
+ });
32600
+ }
32601
+ }
32602
+ });
32603
+ return proof;
32604
+ }
32605
+ latestFreshnessProof(proposalId) {
32606
+ this.requireProposal(proposalId);
32607
+ const row = this.db.prepare(`
32608
+ SELECT payload_json
32609
+ FROM proposal_events
32610
+ WHERE proposal_id = ? AND kind = 'proposal_freshness_checked'
32611
+ ORDER BY event_id DESC
32612
+ LIMIT 1
32613
+ `).get(proposalId);
32614
+ if (!isRecord4(row)) return void 0;
32615
+ try {
32616
+ const payload = JSON.parse(String(row.payload_json));
32617
+ return parseFreshnessProof(payload.proof);
32618
+ } catch {
32619
+ throw new ProposalStoreError(
32620
+ "FRESHNESS_PROOF_TAMPERED",
32621
+ `stored freshness proof for proposal ${proposalId} failed integrity validation`
32622
+ );
32623
+ }
32624
+ }
32625
+ recordFreshnessApprovalBlocked(proposalId, input) {
32626
+ this.requireProposal(proposalId);
32627
+ const proof = this.latestFreshnessProof(proposalId);
32628
+ if (!proof || proof.proof_digest !== input.proof_digest || proof.result === "fresh") {
32629
+ throw new ProposalStoreError(
32630
+ "FRESHNESS_BLOCK_RECORD_INVALID",
32631
+ `freshness block for proposal ${proposalId} does not match its latest non-fresh proof`
32632
+ );
32633
+ }
32634
+ this.appendEvent(proposalId, "proposal_approval_blocked_freshness", input.actor, {
32635
+ proof_digest: proof.proof_digest,
32636
+ safe_code: input.safe_code,
32637
+ result: proof.result
32638
+ });
32639
+ }
32190
32640
  approveProposal(proposalId, options) {
32191
32641
  const proposal = this.requireProposal(proposalId);
32192
32642
  assertWritebackAllowed(proposal, "approved");
@@ -32207,11 +32657,12 @@ var ProposalStore = class {
32207
32657
  if (isRecord4(existing)) {
32208
32658
  throw new ProposalStoreError("APPROVER_ALREADY_COUNTED", `operator ${options.approver} already recorded a decision for proposal ${proposalId}`);
32209
32659
  }
32660
+ this.assertApprovalFreshness(current, options.freshness_proof_digest, now);
32210
32661
  this.db.prepare(`
32211
32662
  INSERT INTO approvals (
32212
32663
  proposal_id, proposal_version, proposal_hash, approver, status, reason,
32213
- identity_json, decision_hash, signature, integrity_hash, created_at
32214
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32664
+ identity_json, decision_hash, signature, integrity_hash, freshness_proof_digest, created_at
32665
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32215
32666
  `).run(
32216
32667
  proposalId,
32217
32668
  options.proposal_version,
@@ -32223,6 +32674,7 @@ var ProposalStore = class {
32223
32674
  options.identity?.decision_hash ?? null,
32224
32675
  options.identity?.signature ?? null,
32225
32676
  options.identity?.integrity_hash ?? null,
32677
+ options.freshness_proof_digest ?? null,
32226
32678
  now
32227
32679
  );
32228
32680
  const progress = this.approvalProgress(proposalId);
@@ -32235,6 +32687,7 @@ var ProposalStore = class {
32235
32687
  proposal_version: options.proposal_version,
32236
32688
  reason: options.reason ?? null,
32237
32689
  identity: publicIdentitySummary(options.identity),
32690
+ freshness_proof_digest: options.freshness_proof_digest ?? null,
32238
32691
  approvals: progress.approved,
32239
32692
  required_approvals: progress.required,
32240
32693
  remaining_approvals: progress.remaining
@@ -32343,17 +32796,31 @@ var ProposalStore = class {
32343
32796
  });
32344
32797
  return;
32345
32798
  }
32799
+ this.assertApprovalFreshness(proposal, options.freshness_proof_digest, now);
32346
32800
  this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("approved", now, proposalId);
32347
32801
  this.db.prepare(`
32348
- INSERT INTO approvals (proposal_id, proposal_version, proposal_hash, approver, status, reason, created_at)
32349
- VALUES (?, ?, ?, ?, ?, ?, ?)
32350
- `).run(proposalId, options.proposal_version, options.proposal_hash, actor, "approved", options.reason, now);
32802
+ INSERT INTO approvals (
32803
+ proposal_id, proposal_version, proposal_hash, approver, status, reason,
32804
+ freshness_proof_digest, created_at
32805
+ )
32806
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
32807
+ `).run(
32808
+ proposalId,
32809
+ options.proposal_version,
32810
+ options.proposal_hash,
32811
+ actor,
32812
+ "approved",
32813
+ options.reason,
32814
+ options.freshness_proof_digest ?? null,
32815
+ now
32816
+ );
32351
32817
  this.appendEvent(proposalId, "proposal_approved", actor, {
32352
32818
  proposal_hash: options.proposal_hash,
32353
32819
  proposal_version: options.proposal_version,
32354
32820
  reason: options.reason,
32355
32821
  policy: options.policy,
32356
- aggregate_limits: options.limits ?? []
32822
+ aggregate_limits: options.limits ?? [],
32823
+ freshness_proof_digest: options.freshness_proof_digest ?? null
32357
32824
  });
32358
32825
  });
32359
32826
  return {
@@ -32701,7 +33168,8 @@ var ProposalStore = class {
32701
33168
  writeback_job_id: receipt.writeback_job_id,
32702
33169
  rows_affected: receipt.rows_affected,
32703
33170
  source_database_mutated: receipt.source_database_mutated,
32704
- receipt_hash: receipt.receipt_hash
33171
+ receipt_hash: receipt.receipt_hash,
33172
+ safe_error_code: "safe_error_code" in receipt ? receipt.safe_error_code ?? null : null
32705
33173
  });
32706
33174
  }
32707
33175
  recordHandlerWritebackJob(input) {
@@ -32785,6 +33253,7 @@ var ProposalStore = class {
32785
33253
  tenant_guard: changeSet.guards.tenant,
32786
33254
  ...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
32787
33255
  allowed_columns: changeSet.guards.allowed_columns,
33256
+ ..."freshness" in changeSet && changeSet.freshness ? { freshness: parseFreshnessAuthority(changeSet.freshness) } : {},
32788
33257
  idempotency_key: `${proposal.proposal_id}:${proposal.object_id}`,
32789
33258
  lease
32790
33259
  };
@@ -33238,7 +33707,21 @@ var ProposalStore = class {
33238
33707
  const key = `${tenantId}\0${capability}`;
33239
33708
  let row = rows.get(key);
33240
33709
  if (!row) {
33241
- row = { tenant_id: tenantId, capability, worker_retries: 0, dead_letters: 0, auto_approval_limit_trips: 0 };
33710
+ row = {
33711
+ tenant_id: tenantId,
33712
+ capability,
33713
+ worker_retries: 0,
33714
+ dead_letters: 0,
33715
+ auto_approval_limit_trips: 0,
33716
+ freshness_checks: 0,
33717
+ freshness_fresh: 0,
33718
+ freshness_stale_target: 0,
33719
+ freshness_stale_supporting: 0,
33720
+ freshness_unavailable: 0,
33721
+ freshness_unsupported: 0,
33722
+ freshness_approval_blocked: 0,
33723
+ freshness_apply_blocked: 0
33724
+ };
33242
33725
  rows.set(key, row);
33243
33726
  }
33244
33727
  return row;
@@ -33246,7 +33729,14 @@ var ProposalStore = class {
33246
33729
  const events2 = this.db.prepare(`
33247
33730
  SELECT p.tenant_id, p.action, e.kind, e.payload_json
33248
33731
  FROM proposal_events e JOIN proposals p ON p.proposal_id = e.proposal_id
33249
- WHERE e.kind IN ('writeback_retry_scheduled', 'writeback_dead_lettered', 'policy_auto_approval_deferred')
33732
+ WHERE e.kind IN (
33733
+ 'writeback_retry_scheduled',
33734
+ 'writeback_dead_lettered',
33735
+ 'policy_auto_approval_deferred',
33736
+ 'proposal_freshness_checked',
33737
+ 'proposal_approval_blocked_freshness',
33738
+ 'writeback_conflict'
33739
+ )
33250
33740
  AND (? IS NULL OR p.tenant_id = ?)
33251
33741
  AND (? IS NULL OR p.action = ?)
33252
33742
  `).all(filters.tenant ?? null, filters.tenant ?? null, filters.capability ?? null, filters.capability ?? null);
@@ -33255,13 +33745,29 @@ var ProposalStore = class {
33255
33745
  const row = ensure(String(raw.tenant_id), String(raw.action));
33256
33746
  if (raw.kind === "writeback_retry_scheduled") row.worker_retries += 1;
33257
33747
  if (raw.kind === "writeback_dead_lettered") row.dead_letters += 1;
33258
- if (raw.kind === "policy_auto_approval_deferred") {
33259
- try {
33260
- const payload = JSON.parse(String(raw.payload_json));
33261
- if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
33262
- } catch {
33748
+ let payload = {};
33749
+ try {
33750
+ const parsed = JSON.parse(String(raw.payload_json));
33751
+ if (isRecord4(parsed)) payload = parsed;
33752
+ } catch {
33753
+ }
33754
+ if (raw.kind === "proposal_freshness_checked") {
33755
+ row.freshness_checks += 1;
33756
+ const proof = isRecord4(payload.proof) ? payload.proof : {};
33757
+ if (proof.result === "fresh") row.freshness_fresh += 1;
33758
+ if (proof.result === "unavailable") row.freshness_unavailable += 1;
33759
+ if (proof.result === "unsupported") row.freshness_unsupported += 1;
33760
+ if (proof.result === "stale") {
33761
+ const checks = Array.isArray(proof.checks) ? proof.checks.filter(isRecord4) : [];
33762
+ if (checks.some((check) => check.kind === "target" && check.status === "stale")) row.freshness_stale_target += 1;
33763
+ if (checks.some((check) => check.kind === "supporting" && check.status === "stale")) row.freshness_stale_supporting += 1;
33263
33764
  }
33264
33765
  }
33766
+ if (raw.kind === "proposal_approval_blocked_freshness") row.freshness_approval_blocked += 1;
33767
+ if (raw.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(payload.safe_error_code ?? ""))) row.freshness_apply_blocked += 1;
33768
+ if (raw.kind === "policy_auto_approval_deferred") {
33769
+ if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
33770
+ }
33265
33771
  }
33266
33772
  return [...rows.values()].sort((left, right) => left.tenant_id.localeCompare(right.tenant_id) || left.capability.localeCompare(right.capability));
33267
33773
  }
@@ -34109,6 +34615,61 @@ var ProposalStore = class {
34109
34615
  }
34110
34616
  return proposal;
34111
34617
  }
34618
+ assertApprovalFreshness(proposal, proofDigest, now) {
34619
+ const authority = proposalFreshnessAuthority(proposal);
34620
+ if (!authority) {
34621
+ if (proofDigest !== void 0) {
34622
+ throw new ProposalStoreError(
34623
+ "FRESHNESS_PROOF_UNEXPECTED",
34624
+ `proposal ${proposal.proposal_id} does not require a freshness proof`
34625
+ );
34626
+ }
34627
+ return;
34628
+ }
34629
+ if (!proofDigest) {
34630
+ throw new ProposalStoreError(
34631
+ "FRESHNESS_PROOF_REQUIRED",
34632
+ `proposal ${proposal.proposal_id} requires a fresh live proof before approval`
34633
+ );
34634
+ }
34635
+ const proof = this.latestFreshnessProof(proposal.proposal_id);
34636
+ if (!proof || proof.proof_digest !== proofDigest) {
34637
+ throw new ProposalStoreError(
34638
+ "FRESHNESS_PROOF_MISSING",
34639
+ `the latest freshness proof for proposal ${proposal.proposal_id} was not supplied`
34640
+ );
34641
+ }
34642
+ if (proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
34643
+ throw new ProposalStoreError(
34644
+ "FRESHNESS_PROOF_AUTHORITY_MISMATCH",
34645
+ `freshness proof does not match proposal ${proposal.proposal_id}`
34646
+ );
34647
+ }
34648
+ if (proof.result !== "fresh") {
34649
+ throw new ProposalStoreError(
34650
+ proof.result === "stale" ? "FRESHNESS_STALE" : "FRESHNESS_NOT_VERIFIED",
34651
+ `proposal ${proposal.proposal_id} freshness result is ${proof.result}`
34652
+ );
34653
+ }
34654
+ if (Date.parse(proof.valid_until) < Date.parse(now)) {
34655
+ throw new ProposalStoreError(
34656
+ "FRESHNESS_PROOF_EXPIRED",
34657
+ `freshness proof for proposal ${proposal.proposal_id} has expired`
34658
+ );
34659
+ }
34660
+ const used = this.db.prepare(`
34661
+ SELECT approval_id
34662
+ FROM approvals
34663
+ WHERE proposal_id = ? AND freshness_proof_digest = ?
34664
+ LIMIT 1
34665
+ `).get(proposal.proposal_id, proofDigest);
34666
+ if (isRecord4(used)) {
34667
+ throw new ProposalStoreError(
34668
+ "FRESHNESS_PROOF_ALREADY_USED",
34669
+ `freshness proof for proposal ${proposal.proposal_id} already authorized a reviewer decision`
34670
+ );
34671
+ }
34672
+ }
34112
34673
  setState(proposalId, state, actor, payload) {
34113
34674
  const now = (/* @__PURE__ */ new Date()).toISOString();
34114
34675
  this.transaction(() => {
@@ -34269,6 +34830,17 @@ function stateFromChangeSet(changeSet) {
34269
34830
  if (changeSet.approval.status === "canceled") return "canceled";
34270
34831
  return "pending_review";
34271
34832
  }
34833
+ function proposalFreshnessAuthority(proposal) {
34834
+ if (!("freshness" in proposal.change_set) || proposal.change_set.freshness === void 0) return void 0;
34835
+ try {
34836
+ return parseFreshnessAuthority(proposal.change_set.freshness);
34837
+ } catch {
34838
+ throw new ProposalStoreError(
34839
+ "FRESHNESS_AUTHORITY_TAMPERED",
34840
+ `stored freshness authority for proposal ${proposal.proposal_id} failed integrity validation`
34841
+ );
34842
+ }
34843
+ }
34272
34844
  function requiredApprovalCount(proposal) {
34273
34845
  const configured = proposal.change_set.approval.required_approvals;
34274
34846
  return typeof configured === "number" && Number.isSafeInteger(configured) && configured >= 1 ? configured : 1;
@@ -34795,6 +35367,7 @@ function rowToApproval(row) {
34795
35367
  decision_hash: row.decision_hash == null ? void 0 : String(row.decision_hash),
34796
35368
  signature: row.signature == null ? void 0 : String(row.signature),
34797
35369
  integrity_hash: row.integrity_hash == null ? void 0 : String(row.integrity_hash),
35370
+ freshness_proof_digest: row.freshness_proof_digest == null ? void 0 : String(row.freshness_proof_digest),
34798
35371
  created_at: String(row.created_at)
34799
35372
  };
34800
35373
  }
@@ -35218,7 +35791,7 @@ var sharedLedgerRestoreSpecs = {
35218
35791
  "updated_at"
35219
35792
  ], ["proposal_id", "proposal_version", "proposal_hash", "action", "state", "tenant_id", "business_object", "object_id", "source_kind", "source_id", "source_schema", "source_table", "source_database_mutated", "change_set_json", "created_at", "updated_at"]),
35220
35793
  proposal_events: restoreSpec("event_id", ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"], ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"]),
35221
- approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
35794
+ approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "freshness_proof_digest", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
35222
35795
  writeback_jobs: restoreSpec("writeback_job_id", ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"], ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"]),
35223
35796
  writeback_intents: restoreSpec("intent_id", ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "result_json", "reconciliation_reason", "created_at", "updated_at"], ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "created_at", "updated_at"]),
35224
35797
  idempotency_receipts: restoreSpec("idempotency_key", ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"], ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"]),
@@ -41327,6 +41900,18 @@ async function callConfiguredTool(input) {
41327
41900
  at: createdAt
41328
41901
  });
41329
41902
  const queryFingerprint = queryFingerprintFor(capability, context);
41903
+ const freshnessCapture = capability.kind === "proposal" ? await captureProposalFreshnessAuthority({
41904
+ config: input.config,
41905
+ capability,
41906
+ args: input.args,
41907
+ context,
41908
+ source,
41909
+ readRow: input.readRow,
41910
+ env: input.env,
41911
+ proposalId,
41912
+ createdAt,
41913
+ targetMemberCount: operation === "insert" ? 0 : setOperation ? currentRows.length : 1
41914
+ }) : void 0;
41330
41915
  const changeSet = capability.kind === "proposal" ? buildChangeSet({
41331
41916
  config: input.config,
41332
41917
  capability,
@@ -41344,7 +41929,8 @@ async function callConfiguredTool(input) {
41344
41929
  resolvedDeduplication,
41345
41930
  evidenceBundleId,
41346
41931
  queryFingerprint,
41347
- objectId
41932
+ objectId,
41933
+ freshness: freshnessCapture?.authority
41348
41934
  }) : void 0;
41349
41935
  await input.store.recordEvidenceBundle({
41350
41936
  evidence_bundle_id: evidenceBundleId,
@@ -41389,6 +41975,48 @@ async function callConfiguredTool(input) {
41389
41975
  }
41390
41976
  });
41391
41977
  }
41978
+ for (const supporting of freshnessCapture?.evidence ?? []) {
41979
+ await input.store.recordEvidenceBundle({
41980
+ evidence_bundle_id: supporting.bundle_id,
41981
+ tenant_id: context.tenant_id,
41982
+ capability: supporting.capability.name,
41983
+ source_id: supporting.capability.source,
41984
+ source_table: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
41985
+ business_object: supporting.capability.target.table,
41986
+ object_id: String(supporting.primary_key.value),
41987
+ query_fingerprint: supporting.query_fingerprint,
41988
+ payload: {
41989
+ kind: "freshness_dependency",
41990
+ dependency_id: supporting.dependency_id,
41991
+ capability: supporting.capability.name,
41992
+ source_id: supporting.capability.source,
41993
+ target: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
41994
+ parameters_redacted: true,
41995
+ source_database_changed: false
41996
+ },
41997
+ items: []
41998
+ });
41999
+ await input.store.recordQueryAudit({
42000
+ evidence_bundle_id: supporting.bundle_id,
42001
+ capability: supporting.capability.name,
42002
+ source_id: supporting.capability.source,
42003
+ query_fingerprint: supporting.query_fingerprint,
42004
+ table_name: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
42005
+ business_object: supporting.capability.target.table,
42006
+ object_id: String(supporting.primary_key.value),
42007
+ primary_key_value: String(supporting.primary_key.value),
42008
+ row_count: 1,
42009
+ payload: {
42010
+ kind: "freshness_dependency",
42011
+ dependency_id: supporting.dependency_id,
42012
+ capability: supporting.capability.name,
42013
+ columns: [supporting.version_column],
42014
+ parameters_redacted: true,
42015
+ tenant_bound: Boolean(supporting.capability.target.tenant_key),
42016
+ principal_bound: Boolean(supporting.capability.target.principal_scope_key)
42017
+ }
42018
+ });
42019
+ }
41392
42020
  if (capability.kind === "read") {
41393
42021
  return {
41394
42022
  status: "ok",
@@ -41438,7 +42066,9 @@ async function callConfiguredTool(input) {
41438
42066
  capability,
41439
42067
  store: input.store,
41440
42068
  proposal,
41441
- patch: changeSet.patch
42069
+ patch: changeSet.patch,
42070
+ env: input.env,
42071
+ readRow: input.readRow
41442
42072
  });
41443
42073
  await input.store.recordEvidenceBundle({
41444
42074
  evidence_bundle_id: evidenceBundleId,
@@ -41486,7 +42116,7 @@ async function callConfiguredTool(input) {
41486
42116
  env: input.env
41487
42117
  });
41488
42118
  return {
41489
- status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
42119
+ status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.freshness?.status === "stale" ? "freshness_conflict" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
41490
42120
  action: capability.name,
41491
42121
  proposal_id: approvalResult.proposal.proposal_id,
41492
42122
  proposal_version: approvalResult.proposal.proposal_version,
@@ -41511,6 +42141,7 @@ async function callConfiguredTool(input) {
41511
42141
  } : {}
41512
42142
  },
41513
42143
  approval_required: approvalResult.proposal.state === "pending_review",
42144
+ ...approvalResult.freshness ? { freshness: approvalResult.freshness } : {},
41514
42145
  governance: input.config.governance?.mode === "cloud_linked" ? { authority: "synapsor_cloud", state: "pending_cloud_sync", evidence_residency: "metadata_only" } : { authority: "local" },
41515
42146
  writeback: changeSet.writeback,
41516
42147
  source_database_changed: false,
@@ -41993,18 +42624,52 @@ async function maybeAutoApproveProposal(input) {
41993
42624
  if (!policy) throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${input.capability.name} references missing approval policy ${policyName}.`);
41994
42625
  const evaluation = evaluateApprovalPolicy(input.capability, policy, input.patch);
41995
42626
  if (!evaluation.qualifies) return { proposal: input.proposal, approved: false, policy: policyName };
42627
+ let freshnessProofDigest;
42628
+ const freshness = await evaluateProposalFreshness({
42629
+ config: input.config,
42630
+ proposal: input.proposal,
42631
+ env: input.env,
42632
+ readRow: input.readRow
42633
+ });
42634
+ if (freshness.required) {
42635
+ await input.store.recordFreshnessProof(freshness.proof);
42636
+ if (freshness.status !== "fresh") {
42637
+ await input.store.recordFreshnessApprovalBlocked(input.proposal.proposal_id, {
42638
+ proof_digest: freshness.proof.proof_digest,
42639
+ safe_code: freshness.safe_code,
42640
+ actor: `policy:${policyName}`
42641
+ });
42642
+ return {
42643
+ proposal: await input.store.getProposal(input.proposal.proposal_id) ?? input.proposal,
42644
+ approved: false,
42645
+ policy: policyName,
42646
+ freshness: {
42647
+ status: freshness.status,
42648
+ safe_code: freshness.safe_code
42649
+ }
42650
+ };
42651
+ }
42652
+ freshnessProofDigest = freshness.proof.proof_digest;
42653
+ }
41996
42654
  const decision = await input.store.approveProposalByPolicy(input.proposal.proposal_id, {
41997
42655
  policy: policyName,
41998
42656
  proposal_hash: input.proposal.proposal_hash,
41999
42657
  proposal_version: input.proposal.proposal_version,
42000
42658
  reason: `auto-approved by policy ${policyName}: ${evaluation.reason}`,
42001
- limits: policy.limits
42659
+ limits: policy.limits,
42660
+ freshness_proof_digest: freshnessProofDigest
42002
42661
  });
42003
42662
  return {
42004
42663
  proposal: decision.proposal,
42005
42664
  approved: decision.approved,
42006
42665
  policy: policyName,
42007
- tripped_limits: decision.tripped_limits
42666
+ tripped_limits: decision.tripped_limits,
42667
+ ...freshness.required ? {
42668
+ freshness: {
42669
+ status: freshness.status,
42670
+ safe_code: freshness.safe_code
42671
+ }
42672
+ } : {}
42008
42673
  };
42009
42674
  }
42010
42675
  function approvalPolicyByName(config, policyName) {
@@ -42272,6 +42937,372 @@ function errorMessage(error) {
42272
42937
  if (isRecord6(error) && typeof error.message === "string") return error.message;
42273
42938
  return typeof error === "string" ? error : "";
42274
42939
  }
42940
+ async function captureProposalFreshnessAuthority(input) {
42941
+ const policy = input.config.proposal_freshness?.[input.capability.name];
42942
+ if (!policy) return void 0;
42943
+ const operation = input.capability.operation?.kind ?? "update";
42944
+ const targetMode = operation === "insert" ? "not_applicable" : isSetCapability(input.capability) ? "frozen_set" : "exact_guard";
42945
+ const captured = [];
42946
+ for (const configured of policy.dependencies ?? []) {
42947
+ const capability = (input.config.capabilities ?? []).find((item) => item.name === configured.capability);
42948
+ if (!capability || capability.kind !== "read") {
42949
+ throw new McpRuntimeError("FRESHNESS_DEPENDENCY_INVALID", `Reviewed freshness dependency ${configured.id} is unavailable.`);
42950
+ }
42951
+ const identity = scalar3(input.args[configured.identity_from_arg]);
42952
+ if (identity === null) {
42953
+ throw new McpRuntimeError("FRESHNESS_DEPENDENCY_IDENTITY_MISSING", `Freshness dependency ${configured.id} requires its reviewed scalar identity.`);
42954
+ }
42955
+ const readCapability = {
42956
+ ...capability,
42957
+ conflict_guard: { column: configured.version_column }
42958
+ };
42959
+ const current = await input.readRow({
42960
+ sourceName: capability.source,
42961
+ source: input.source,
42962
+ capability: readCapability,
42963
+ args: { [capability.lookup.id_from_arg]: identity },
42964
+ context: input.context,
42965
+ env: input.env,
42966
+ transaction_mode: "read_only"
42967
+ });
42968
+ if (current.rowCount !== 1) {
42969
+ throw new McpRuntimeError("FRESHNESS_DEPENDENCY_UNRESOLVED", `Freshness dependency ${configured.id} did not resolve to one authorized row.`);
42970
+ }
42971
+ const primaryValue = scalar3(current.row[capability.target.primary_key]);
42972
+ const versionValue = scalar3(current.row[configured.version_column]);
42973
+ if (primaryValue === null || versionValue === null) {
42974
+ throw new McpRuntimeError("FRESHNESS_DEPENDENCY_VERSION_MISSING", `Freshness dependency ${configured.id} lacks an exact primary-key or version value.`);
42975
+ }
42976
+ const bundleId = stableId("evf", {
42977
+ proposal_id: input.proposalId,
42978
+ dependency_id: configured.id,
42979
+ primary_key: primaryValue,
42980
+ created_at: input.createdAt
42981
+ });
42982
+ const queryFingerprint = queryFingerprintFor(readCapability, input.context);
42983
+ const unsigned2 = {
42984
+ id: configured.id,
42985
+ capability: configured.capability,
42986
+ source_id: capability.source,
42987
+ engine: input.source.engine,
42988
+ target: {
42989
+ schema: capability.target.schema,
42990
+ table: capability.target.table,
42991
+ primary_key: { column: capability.target.primary_key, value: primaryValue },
42992
+ tenant_column: capability.target.tenant_key ?? "__single_tenant_dev",
42993
+ ...capability.target.principal_scope_key ? { principal_column: capability.target.principal_scope_key } : {}
42994
+ },
42995
+ expected_version: { column: configured.version_column, value: conflictGuardScalar(versionValue) },
42996
+ evidence: {
42997
+ bundle_id: bundleId,
42998
+ query_fingerprint: queryFingerprint
42999
+ }
43000
+ };
43001
+ const descriptor = {
43002
+ ...unsigned2,
43003
+ descriptor_digest: canonicalJsonDigest(unsigned2)
43004
+ };
43005
+ captured.push({
43006
+ descriptor,
43007
+ evidence: {
43008
+ dependency_id: configured.id,
43009
+ bundle_id: bundleId,
43010
+ query_fingerprint: queryFingerprint,
43011
+ capability,
43012
+ primary_key: descriptor.target.primary_key,
43013
+ version_column: configured.version_column
43014
+ }
43015
+ });
43016
+ }
43017
+ captured.sort((left, right) => left.descriptor.source_id.localeCompare(right.descriptor.source_id) || left.descriptor.target.schema.localeCompare(right.descriptor.target.schema) || left.descriptor.target.table.localeCompare(right.descriptor.target.table) || JSON.stringify(left.descriptor.target.primary_key.value).localeCompare(JSON.stringify(right.descriptor.target.primary_key.value)) || left.descriptor.id.localeCompare(right.descriptor.id));
43018
+ const unsigned = {
43019
+ schema_version: protocolVersions.freshnessAuthority,
43020
+ required: true,
43021
+ target: { mode: targetMode, member_count: input.targetMemberCount },
43022
+ dependencies: captured.map((item) => item.descriptor)
43023
+ };
43024
+ const authority = parseFreshnessAuthority({
43025
+ ...unsigned,
43026
+ dependency_set_digest: canonicalJsonDigest(unsigned)
43027
+ });
43028
+ return {
43029
+ authority,
43030
+ evidence: captured.map((item) => item.evidence)
43031
+ };
43032
+ }
43033
+ async function evaluateProposalFreshness(input) {
43034
+ const config = resolveRuntimeConfig(input.config);
43035
+ assertValidRunnerCapabilityConfig(config);
43036
+ const authority = freshnessAuthorityFromChangeSet(input.proposal.change_set);
43037
+ if (!authority) {
43038
+ return {
43039
+ required: false,
43040
+ status: "not_required",
43041
+ safe_code: "FRESHNESS_NOT_REQUIRED",
43042
+ target_count: 0,
43043
+ supporting_count: 0
43044
+ };
43045
+ }
43046
+ const env = input.env ?? process.env;
43047
+ const resources = createMcpRuntimeSharedResources(config, env, input.readRow, input.clock, input.credentialResolver);
43048
+ const checkedAtMs = (input.clock ?? Date.now)();
43049
+ const checks = [];
43050
+ let forcedResult;
43051
+ let forcedCode;
43052
+ try {
43053
+ const capability = (config.capabilities ?? []).find((item) => item.name === input.proposal.action);
43054
+ const source = capability ? config.sources?.[capability.source] : void 0;
43055
+ if (!capability || capability.kind !== "proposal" || !source) {
43056
+ forcedResult = "invalid";
43057
+ forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_INVALID";
43058
+ } else if (!proposalAuthorityMatchesCapability(input.proposal, capability)) {
43059
+ forcedResult = "invalid";
43060
+ forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_MISMATCH";
43061
+ } else {
43062
+ const context = proposalTrustedContext(config, capability, input.proposal);
43063
+ const targetChecks = await evaluateTargetFreshness({
43064
+ proposal: input.proposal,
43065
+ authority,
43066
+ capability,
43067
+ source,
43068
+ context,
43069
+ env,
43070
+ readRow: resources.readRow
43071
+ });
43072
+ checks.push(...targetChecks);
43073
+ for (const dependency of authority.dependencies) {
43074
+ const validationCode = validateResolvedFreshnessDependency(config, capability, dependency);
43075
+ if (validationCode) {
43076
+ checks.push({
43077
+ id: dependency.id,
43078
+ kind: "supporting",
43079
+ status: "invalid",
43080
+ safe_code: validationCode
43081
+ });
43082
+ continue;
43083
+ }
43084
+ const supporting = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
43085
+ checks.push(await evaluateSupportingFreshness({
43086
+ dependency,
43087
+ capability: supporting,
43088
+ source,
43089
+ context,
43090
+ env,
43091
+ readRow: resources.readRow
43092
+ }));
43093
+ }
43094
+ }
43095
+ } catch (error) {
43096
+ const classified = classifyFreshnessReadError(error);
43097
+ forcedResult = classified.result;
43098
+ forcedCode = classified.safe_code;
43099
+ } finally {
43100
+ await resources.close();
43101
+ }
43102
+ const status = forcedResult ?? freshnessResultForChecks(checks);
43103
+ const safeCode2 = forcedCode ?? freshnessSafeCode(status, checks);
43104
+ const targetCount = checks.filter((check) => check.kind === "target").length;
43105
+ const supportingCount = checks.filter((check) => check.kind === "supporting").length;
43106
+ const adapters2 = freshnessSourceAdapters(input.proposal, authority);
43107
+ const unsigned = {
43108
+ schema_version: protocolVersions.freshnessProof,
43109
+ proposal_id: input.proposal.proposal_id,
43110
+ proposal_hash: input.proposal.proposal_hash,
43111
+ proposal_version: input.proposal.proposal_version,
43112
+ dependency_set_digest: authority.dependency_set_digest,
43113
+ checked_at: new Date(checkedAtMs).toISOString(),
43114
+ valid_until: new Date(checkedAtMs + Math.max(1e3, Math.min(input.proofValidityMs ?? 3e4, 3e5))).toISOString(),
43115
+ source_adapters: adapters2,
43116
+ result: status,
43117
+ safe_code: safeCode2,
43118
+ target_count: targetCount,
43119
+ supporting_count: supportingCount,
43120
+ checks
43121
+ };
43122
+ const proof = parseFreshnessProof({
43123
+ ...unsigned,
43124
+ proof_digest: canonicalJsonDigest(unsigned)
43125
+ });
43126
+ return {
43127
+ required: true,
43128
+ status,
43129
+ safe_code: safeCode2,
43130
+ target_count: targetCount,
43131
+ supporting_count: supportingCount,
43132
+ proof
43133
+ };
43134
+ }
43135
+ function freshnessAuthorityFromChangeSet(changeSet) {
43136
+ if (!("freshness" in changeSet) || changeSet.freshness === void 0) return void 0;
43137
+ return parseFreshnessAuthority(changeSet.freshness);
43138
+ }
43139
+ function proposalAuthorityMatchesCapability(proposal, capability) {
43140
+ const changeSet = proposal.change_set;
43141
+ if (proposal.proposal_hash !== changeSet.integrity.proposal_hash || proposal.proposal_version !== changeSet.proposal_version) return false;
43142
+ if (changeSet.source.source_id !== capability.source || changeSet.source.schema !== capability.target.schema || changeSet.source.table !== capability.target.table || changeSet.source.primary_key.column !== capability.target.primary_key) return false;
43143
+ if (changeSet.contract && (!capability.contract_provenance || changeSet.contract.digest !== capability.contract_provenance.digest || changeSet.contract.version !== capability.contract_provenance.version)) return false;
43144
+ return true;
43145
+ }
43146
+ function proposalTrustedContext(config, capability, proposal) {
43147
+ const contextConfig = (capability.context ? config.contexts?.[capability.context] : void 0) ?? config.trusted_context;
43148
+ const provider = contextConfig?.provider ?? (proposal.change_set.principal.source === "cloud_session" ? "cloud_session" : "environment");
43149
+ return {
43150
+ tenant_id: proposal.tenant_id,
43151
+ principal: proposal.principal ?? proposal.change_set.principal.id,
43152
+ provenance: provider
43153
+ };
43154
+ }
43155
+ async function evaluateTargetFreshness(input) {
43156
+ if (input.authority.target.mode === "not_applicable") return [];
43157
+ if (input.authority.target.mode === "frozen_set") {
43158
+ const changeSet2 = input.proposal.change_set;
43159
+ if (changeSet2.schema_version !== protocolVersions.changeSetV3) {
43160
+ return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_SET_INVALID" }];
43161
+ }
43162
+ const current2 = await input.readRow({
43163
+ sourceName: input.capability.source,
43164
+ source: input.source,
43165
+ capability: input.capability,
43166
+ args: {},
43167
+ context: input.context,
43168
+ env: input.env,
43169
+ transaction_mode: "read_only"
43170
+ });
43171
+ const rows = current2.rows ?? (current2.rowCount === 1 ? [current2.row] : []);
43172
+ const byIdentity = new Map(rows.map((row) => [JSON.stringify(scalar3(row[input.capability.target.primary_key])), row]));
43173
+ return changeSet2.frozen_set.members.map((member) => {
43174
+ const expected2 = member.expected_version;
43175
+ const row = byIdentity.get(JSON.stringify(member.primary_key.value));
43176
+ const observed2 = expected2 && row ? conflictGuardScalar(scalar3(row[expected2.column])) : void 0;
43177
+ const fresh2 = Boolean(expected2 && row && versionsEqual(observed2, expected2.value));
43178
+ return {
43179
+ id: `target:${canonicalJsonDigest(member.primary_key).slice(7, 23)}`,
43180
+ kind: "target",
43181
+ status: fresh2 ? "fresh" : "stale",
43182
+ safe_code: fresh2 ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
43183
+ ...expected2 ? { expected_version_digest: versionMetadataDigest(expected2.column, expected2.value) } : {},
43184
+ ...expected2 && observed2 !== void 0 ? { observed_version_digest: versionMetadataDigest(expected2.column, observed2) } : {}
43185
+ };
43186
+ });
43187
+ }
43188
+ const changeSet = input.proposal.change_set;
43189
+ if (changeSet.schema_version === protocolVersions.changeSetV3 || changeSet.schema_version === protocolVersions.compensationChangeSet) {
43190
+ return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_AUTHORITY_INVALID" }];
43191
+ }
43192
+ const expected = changeSet.guards.expected_version;
43193
+ if (!expected || expected.column === "__row_hash" || changeSet.source.primary_key.value === void 0) {
43194
+ return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED" }];
43195
+ }
43196
+ const readCapability = {
43197
+ ...input.capability,
43198
+ conflict_guard: { column: expected.column }
43199
+ };
43200
+ const current = await input.readRow({
43201
+ sourceName: input.capability.source,
43202
+ source: input.source,
43203
+ capability: readCapability,
43204
+ args: { [input.capability.lookup.id_from_arg]: changeSet.source.primary_key.value },
43205
+ context: input.context,
43206
+ env: input.env,
43207
+ transaction_mode: "read_only"
43208
+ });
43209
+ const observed = current.rowCount === 1 ? conflictGuardScalar(scalar3(current.row[expected.column])) : void 0;
43210
+ const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, expected.value);
43211
+ return [{
43212
+ id: "target",
43213
+ kind: "target",
43214
+ status: fresh ? "fresh" : "stale",
43215
+ safe_code: fresh ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
43216
+ expected_version_digest: versionMetadataDigest(expected.column, expected.value),
43217
+ ...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(expected.column, observed) } : {}
43218
+ }];
43219
+ }
43220
+ function validateResolvedFreshnessDependency(config, proposalCapability, dependency) {
43221
+ const policy = config.proposal_freshness?.[proposalCapability.name];
43222
+ const declared = policy?.dependencies?.find((item) => item.id === dependency.id);
43223
+ const capability = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
43224
+ const source = config.sources?.[dependency.source_id];
43225
+ if (!declared || declared.capability !== dependency.capability || declared.version_column !== dependency.expected_version.column) return "FRESHNESS_DEPENDENCY_AUTHORITY_MISMATCH";
43226
+ if (!capability || capability.kind !== "read" || !source) return "FRESHNESS_DEPENDENCY_AUTHORITY_INVALID";
43227
+ if (capability.source !== proposalCapability.source || dependency.source_id !== capability.source || dependency.engine !== source.engine) return "FRESHNESS_CROSS_SOURCE_UNSUPPORTED";
43228
+ if (capability.target.schema !== dependency.target.schema || capability.target.table !== dependency.target.table || capability.target.primary_key !== dependency.target.primary_key.column || (capability.target.tenant_key ?? "__single_tenant_dev") !== dependency.target.tenant_column || capability.target.principal_scope_key !== dependency.target.principal_column) return "FRESHNESS_DEPENDENCY_TARGET_MISMATCH";
43229
+ return void 0;
43230
+ }
43231
+ async function evaluateSupportingFreshness(input) {
43232
+ const readCapability = {
43233
+ ...input.capability,
43234
+ conflict_guard: { column: input.dependency.expected_version.column }
43235
+ };
43236
+ try {
43237
+ const current = await input.readRow({
43238
+ sourceName: input.dependency.source_id,
43239
+ source: input.source,
43240
+ capability: readCapability,
43241
+ args: { [input.capability.lookup.id_from_arg]: input.dependency.target.primary_key.value },
43242
+ context: input.context,
43243
+ env: input.env,
43244
+ transaction_mode: "read_only"
43245
+ });
43246
+ const observed = current.rowCount === 1 ? conflictGuardScalar(scalar3(current.row[input.dependency.expected_version.column])) : void 0;
43247
+ const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, input.dependency.expected_version.value);
43248
+ return {
43249
+ id: input.dependency.id,
43250
+ kind: "supporting",
43251
+ status: fresh ? "fresh" : "stale",
43252
+ safe_code: fresh ? "FRESHNESS_DEPENDENCY_FRESH" : "FRESHNESS_DEPENDENCY_STALE",
43253
+ expected_version_digest: versionMetadataDigest(input.dependency.expected_version.column, input.dependency.expected_version.value),
43254
+ ...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(input.dependency.expected_version.column, observed) } : {}
43255
+ };
43256
+ } catch (error) {
43257
+ const classified = classifyFreshnessReadError(error);
43258
+ return {
43259
+ id: input.dependency.id,
43260
+ kind: "supporting",
43261
+ status: classified.result,
43262
+ safe_code: classified.safe_code
43263
+ };
43264
+ }
43265
+ }
43266
+ function freshnessResultForChecks(checks) {
43267
+ if (checks.some((check) => check.status === "invalid")) return "invalid";
43268
+ if (checks.some((check) => check.status === "unsupported")) return "unsupported";
43269
+ if (checks.some((check) => check.status === "unavailable")) return "unavailable";
43270
+ if (checks.some((check) => check.status === "stale")) return "stale";
43271
+ return "fresh";
43272
+ }
43273
+ function freshnessSafeCode(status, checks) {
43274
+ if (status === "fresh") return "FRESHNESS_FRESH";
43275
+ if (status === "stale" && checks.some((check) => check.kind === "target" && check.status === "stale")) return "FRESHNESS_TARGET_STALE";
43276
+ if (status === "stale") return "FRESHNESS_DEPENDENCY_STALE";
43277
+ if (status === "unavailable") return "FRESHNESS_TEMPORARILY_UNAVAILABLE";
43278
+ if (status === "unsupported") return "FRESHNESS_TOPOLOGY_UNSUPPORTED";
43279
+ return "FRESHNESS_AUTHORITY_INVALID";
43280
+ }
43281
+ function classifyFreshnessReadError(error) {
43282
+ const code = error instanceof McpRuntimeError ? error.code : errorStringProperty(error, "code") ?? "";
43283
+ if (/(TIMEOUT|POOL|CONNECTION|ECONN|ETIMEDOUT|TOO_MANY|UNAVAILABLE|SATURAT)/i.test(`${code} ${errorMessage(error)}`)) {
43284
+ return { result: "unavailable", safe_code: "FRESHNESS_TEMPORARILY_UNAVAILABLE" };
43285
+ }
43286
+ if (/(UNSUPPORTED|CROSS_SOURCE)/i.test(code)) {
43287
+ return { result: "unsupported", safe_code: "FRESHNESS_TOPOLOGY_UNSUPPORTED" };
43288
+ }
43289
+ return { result: "invalid", safe_code: "FRESHNESS_CHECK_FAILED" };
43290
+ }
43291
+ function freshnessSourceAdapters(proposal, authority) {
43292
+ const entries = /* @__PURE__ */ new Map();
43293
+ const targetEngine = proposal.source_kind === "external_mysql" ? "mysql" : "postgres";
43294
+ entries.set(`${proposal.source_id}:${targetEngine}`, { source_id: proposal.source_id, engine: targetEngine });
43295
+ for (const dependency of authority.dependencies) {
43296
+ entries.set(`${dependency.source_id}:${dependency.engine}`, { source_id: dependency.source_id, engine: dependency.engine });
43297
+ }
43298
+ return [...entries.values()].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.engine.localeCompare(right.engine));
43299
+ }
43300
+ function versionsEqual(left, right) {
43301
+ return canonicalJsonDigest({ value: conflictGuardScalar(scalar3(left)) }) === canonicalJsonDigest({ value: conflictGuardScalar(scalar3(right)) });
43302
+ }
43303
+ function versionMetadataDigest(column, value) {
43304
+ return canonicalJsonDigest({ column, value: conflictGuardScalar(scalar3(value)) });
43305
+ }
42275
43306
  function effectivePrincipalScope(config, capability, context) {
42276
43307
  const column = capability.target.principal_scope_key;
42277
43308
  if (!column) return void 0;
@@ -42350,6 +43381,7 @@ function buildChangeSet(input) {
42350
43381
  ...input.capability.operation.version_advance ? { version_advance: input.capability.operation.version_advance } : {},
42351
43382
  ...input.resolvedDeduplication ? { deduplication: input.resolvedDeduplication } : {}
42352
43383
  },
43384
+ ...input.freshness ? { freshness: input.freshness } : {},
42353
43385
  ...input.capability.reversibility ? {
42354
43386
  reversibility: {
42355
43387
  mode: "reviewed_inverse",
@@ -42415,6 +43447,7 @@ function buildChangeSet(input) {
42415
43447
  allowed_columns: input.capability.allowed_columns ?? Object.keys(patch),
42416
43448
  expected_version: guard
42417
43449
  },
43450
+ ...input.freshness ? { freshness: input.freshness } : {},
42418
43451
  evidence: {
42419
43452
  bundle_id: input.evidenceBundleId,
42420
43453
  query_fingerprint: input.queryFingerprint,
@@ -42548,6 +43581,7 @@ function buildBoundedSetChangeSet(input) {
42548
43581
  allowed_columns: kind === "set_delete" ? [] : input.capability.allowed_columns ?? Object.keys(input.patch),
42549
43582
  ...kind === "set_update" && operation.version_advance ? { version_advance: operation.version_advance } : {}
42550
43583
  },
43584
+ ...input.freshness ? { freshness: input.freshness } : {},
42551
43585
  frozen_set: frozenSet,
42552
43586
  ...input.capability.reversibility ? {
42553
43587
  reversibility: {
@@ -42747,7 +43781,7 @@ var RuntimeDatabasePools = class {
42747
43781
  counter.active += 1;
42748
43782
  try {
42749
43783
  const query = runtimeReadQuery(input.capability, "$", input.args, input.context);
42750
- await client.query(input.capability.protected_read ? "BEGIN READ ONLY" : "BEGIN");
43784
+ await client.query(input.capability.protected_read || input.transaction_mode === "read_only" ? "BEGIN READ ONLY" : "BEGIN");
42751
43785
  const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
42752
43786
  if (timeoutMs) await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
42753
43787
  if (input.source.database_scope?.mode === "postgres_rls") {
@@ -42814,14 +43848,15 @@ var RuntimeDatabasePools = class {
42814
43848
  const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
42815
43849
  if (timeoutMs) await connection.query("SET SESSION max_execution_time = ?", [timeoutMs]).catch(() => void 0);
42816
43850
  const query = runtimeReadQuery(input.capability, "?", input.args, input.context);
42817
- if (input.capability.protected_read) await connection.query("START TRANSACTION READ ONLY");
43851
+ const readOnlyTransaction = Boolean(input.capability.protected_read || input.transaction_mode === "read_only");
43852
+ if (readOnlyTransaction) await connection.query("START TRANSACTION READ ONLY");
42818
43853
  try {
42819
43854
  const [rows] = await connection.execute(query.sql, query.values.map(scalar3));
42820
43855
  const list = Array.isArray(rows) ? rows : [];
42821
- if (input.capability.protected_read) await connection.query("COMMIT");
43856
+ if (readOnlyTransaction) await connection.query("COMMIT");
42822
43857
  return { row: list[0] ?? {}, rows: list, rowCount: list.length };
42823
43858
  } catch (error) {
42824
- if (input.capability.protected_read) await connection.query("ROLLBACK").catch(() => void 0);
43859
+ if (readOnlyTransaction) await connection.query("ROLLBACK").catch(() => void 0);
42825
43860
  throw error;
42826
43861
  }
42827
43862
  } finally {
@@ -43975,6 +45010,15 @@ function validateOperation2(job) {
43975
45010
  }
43976
45011
  async function mutateMysql(job, connection) {
43977
45012
  if (job.protocol_version === "4.0") return await mutateMysqlCompensation(job, connection);
45013
+ const freshnessConflict = await lockMysqlFreshnessDependencies(job, connection);
45014
+ if (freshnessConflict) {
45015
+ return {
45016
+ status: "conflict",
45017
+ affectedRows: 0,
45018
+ code: freshnessConflict,
45019
+ targetIdentity: identityForJob2(job)
45020
+ };
45021
+ }
43978
45022
  if (job.protocol_version === "3.0") return await mutateMysqlSet(job, connection);
43979
45023
  const operation = operationOf2(job);
43980
45024
  if (operation === "single_row_insert") return await insertMysql(job, connection);
@@ -44035,6 +45079,38 @@ EXISTS (SELECT 1 FROM information_schema.USER_PRIVILEGES WHERE REPLACE(GRANTEE,
44035
45079
  }
44036
45080
  return { status: "applied", affectedRows: 1, targetIdentity: identityForJob2(job), resultVersion, beforeDigest, afterDigest: digest2({ identity: identityForJob2(job), patch: job.patch, version: resultVersion }) };
44037
45081
  }
45082
+ function freshnessDependencies2(job) {
45083
+ if (!("freshness" in job) || !job.freshness) return [];
45084
+ return job.freshness.dependencies;
45085
+ }
45086
+ async function lockMysqlFreshnessDependencies(job, connection) {
45087
+ for (const dependency of freshnessDependencies2(job)) {
45088
+ const where = [
45089
+ `${quoteMysqlIdentifier(dependency.target.primary_key.column)} = ?`,
45090
+ `${quoteMysqlIdentifier(dependency.target.tenant_column)} = ?`
45091
+ ];
45092
+ const values = [
45093
+ dependency.target.primary_key.value,
45094
+ job.target.tenant_guard.value
45095
+ ];
45096
+ if (dependency.target.principal_column) {
45097
+ const scope = principalScope2(job);
45098
+ if (!scope) return "FRESHNESS_DEPENDENCY_SCOPE_INVALID";
45099
+ where.push(`${quoteMysqlIdentifier(dependency.target.principal_column)} = ?`);
45100
+ values.push(scope.value);
45101
+ }
45102
+ const [rows] = await connection.query(
45103
+ `SELECT ${quoteMysqlIdentifier(dependency.expected_version.column)} AS __synapsor_freshness_version FROM ${quoteMysqlIdentifier(dependency.target.schema)}.${quoteMysqlIdentifier(dependency.target.table)} WHERE ${where.join(" AND ")} FOR UPDATE`,
45104
+ values
45105
+ );
45106
+ if (rows.length !== 1) return "FRESHNESS_DEPENDENCY_STALE";
45107
+ if (!versionValuesMatch2(
45108
+ rows[0]?.__synapsor_freshness_version,
45109
+ dependency.expected_version.value
45110
+ )) return "FRESHNESS_DEPENDENCY_STALE";
45111
+ }
45112
+ return void 0;
45113
+ }
44038
45114
  function compensationProjection2(job) {
44039
45115
  const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
44040
45116
  const scope = principalScope2(job);
@@ -45826,7 +46902,7 @@ import path3 from "node:path";
45826
46902
  // apps/runner/package.json
45827
46903
  var package_default = {
45828
46904
  name: "@synapsor/runner",
45829
- version: "1.6.0",
46905
+ version: "1.6.1",
45830
46906
  description: "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
45831
46907
  license: "Apache-2.0",
45832
46908
  type: "module",
@@ -50859,10 +51935,11 @@ async function startLocalUiServer(options = {}) {
50859
51935
  const projectRoot = path14.resolve(options.projectRoot ?? path14.dirname(configPath));
50860
51936
  const boundaryRoot = options.boundaryRoot ? path14.resolve(options.boundaryRoot) : void 0;
50861
51937
  const safeActionPreview = options.safeActionPreview ?? executeSafeActionPreview;
51938
+ const freshnessEvaluator = options.freshnessEvaluator ?? ((proposal) => evaluateWorkbenchFreshness(configPath, proposal));
50862
51939
  const bootstrapState = { consumed: false };
50863
51940
  const server = createServer2(async (request, response) => {
50864
51941
  try {
50865
- await handleRequest({ request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, token, csrfToken, tour: options.tour === true, bootstrapState });
51942
+ await handleRequest({ request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, freshnessEvaluator, token, csrfToken, tour: options.tour === true, bootstrapState });
50866
51943
  } catch (error) {
50867
51944
  sendJson(response, 500, {
50868
51945
  ok: false,
@@ -50896,7 +51973,7 @@ async function startLocalUiServer(options = {}) {
50896
51973
  };
50897
51974
  }
50898
51975
  async function handleRequest(input) {
50899
- const { request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, token, csrfToken, tour, bootstrapState } = input;
51976
+ const { request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, freshnessEvaluator, token, csrfToken, tour, bootstrapState } = input;
50900
51977
  const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
50901
51978
  if (request.method === "GET" && url.pathname === "/" && url.searchParams.has("token")) {
50902
51979
  if (url.searchParams.get("token") !== token || bootstrapState.consumed) {
@@ -51207,11 +52284,32 @@ async function handleRequest(input) {
51207
52284
  data_pr: buildDataPr(proposal, reviewView, receipts2.at(-1)),
51208
52285
  events: store.events(proposalId),
51209
52286
  receipts: receipts2,
51210
- evidence: store.getEvidenceBundle(proposal.change_set.evidence.bundle_id)
52287
+ evidence: store.getEvidenceBundle(proposal.change_set.evidence.bundle_id),
52288
+ freshness: storedFreshnessSummary(proposal, store.latestFreshnessProof(proposalId))
51211
52289
  });
51212
52290
  });
51213
52291
  return;
51214
52292
  }
52293
+ const freshnessMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)\/check-freshness$/);
52294
+ if (request.method === "POST" && freshnessMatch) {
52295
+ if (!hasValidCsrf(request, csrfToken)) {
52296
+ sendJson(response, 403, { ok: false, error: "CSRF token required for proposal freshness checks" });
52297
+ return;
52298
+ }
52299
+ const proposalId = decodeURIComponent(freshnessMatch[1] ?? "");
52300
+ const proposal = await storeAccess("read", "proposal-freshness-read", (store) => requireProposal(store, proposalId));
52301
+ const freshness = await freshnessEvaluator(proposal);
52302
+ if (freshness.required) {
52303
+ await storeAccess("write", "proposal-freshness-record", (store) => {
52304
+ store.recordFreshnessProof(freshness.proof);
52305
+ });
52306
+ }
52307
+ sendJson(response, freshnessHttpStatus(freshness), {
52308
+ ok: freshness.status === "fresh" || freshness.status === "not_required",
52309
+ freshness: workbenchFreshnessSummary(freshness)
52310
+ });
52311
+ return;
52312
+ }
51215
52313
  const approveMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)\/approve$/);
51216
52314
  if (request.method === "POST" && approveMatch) {
51217
52315
  if (!hasValidCsrf(request, csrfToken)) {
@@ -51229,15 +52327,45 @@ async function handleRequest(input) {
51229
52327
  const proposalId = decodeURIComponent(approveMatch[1] ?? "");
51230
52328
  const body = await readJsonBody(request);
51231
52329
  if (body.confirm !== "approve") throw new Error("approval requires confirm=approve");
52330
+ const proposalForCheck = await storeAccess("read", "proposal-approve-freshness-read", (store) => requireProposal(store, proposalId));
52331
+ const freshness = await freshnessEvaluator(proposalForCheck);
52332
+ if (freshness.required) {
52333
+ await storeAccess("write", "proposal-approve-freshness-record", (store) => {
52334
+ store.recordFreshnessProof(freshness.proof);
52335
+ });
52336
+ }
52337
+ if (freshness.status !== "fresh" && freshness.status !== "not_required") {
52338
+ if (freshness.required) {
52339
+ await storeAccess("write", "proposal-approve-freshness-blocked", (store) => {
52340
+ store.recordFreshnessApprovalBlocked(proposalId, {
52341
+ proof_digest: freshness.proof.proof_digest,
52342
+ safe_code: freshness.safe_code,
52343
+ actor: stringOrDefault(body.actor, "local_reviewer")
52344
+ });
52345
+ });
52346
+ }
52347
+ sendJson(response, freshnessHttpStatus(freshness), {
52348
+ ok: false,
52349
+ error: freshness.status === "stale" ? "Proposal or supporting evidence is stale. Create a new source read and proposal." : "Freshness could not be verified. No approval was recorded.",
52350
+ freshness: workbenchFreshnessSummary(freshness)
52351
+ });
52352
+ return;
52353
+ }
51232
52354
  await storeAccess("write", "proposal-approve", (store) => {
51233
52355
  const proposal = requireProposal(store, proposalId);
51234
52356
  const updated = store.approveProposal(proposalId, {
51235
52357
  approver: stringOrDefault(body.actor, "local_reviewer"),
51236
52358
  proposal_hash: proposal.proposal_hash,
51237
52359
  proposal_version: proposal.proposal_version,
51238
- reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0
52360
+ reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0,
52361
+ freshness_proof_digest: freshness.required ? freshness.proof.proof_digest : void 0
52362
+ });
52363
+ sendJson(response, 200, {
52364
+ ok: true,
52365
+ proposal: updated,
52366
+ approval_progress: store.approvalProgress(proposalId),
52367
+ freshness: workbenchFreshnessSummary(freshness)
51239
52368
  });
51240
- sendJson(response, 200, { ok: true, proposal: updated, approval_progress: store.approvalProgress(proposalId) });
51241
52369
  });
51242
52370
  return;
51243
52371
  }
@@ -51282,6 +52410,65 @@ async function handleRequest(input) {
51282
52410
  }
51283
52411
  sendJson(response, 404, { ok: false, error: "not found" });
51284
52412
  }
52413
+ async function evaluateWorkbenchFreshness(configPath, proposal) {
52414
+ const required = "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0;
52415
+ if (!required) {
52416
+ return {
52417
+ required: false,
52418
+ status: "not_required",
52419
+ safe_code: "FRESHNESS_NOT_REQUIRED",
52420
+ target_count: 0,
52421
+ supporting_count: 0
52422
+ };
52423
+ }
52424
+ const config = await loadRuntimeConfigFromFile(configPath);
52425
+ return evaluateProposalFreshness({ config, proposal, env: process.env });
52426
+ }
52427
+ function workbenchFreshnessSummary(result) {
52428
+ if (!result.required) {
52429
+ return {
52430
+ required: false,
52431
+ status: "not_required",
52432
+ safe_code: result.safe_code,
52433
+ target_count: 0,
52434
+ supporting_count: 0
52435
+ };
52436
+ }
52437
+ return {
52438
+ required: true,
52439
+ status: result.status,
52440
+ safe_code: result.safe_code,
52441
+ checked_at: result.proof.checked_at,
52442
+ valid_until: result.proof.valid_until,
52443
+ proof_digest: result.proof.proof_digest,
52444
+ target_count: result.target_count,
52445
+ supporting_count: result.supporting_count,
52446
+ checks: result.proof.checks
52447
+ };
52448
+ }
52449
+ function storedFreshnessSummary(proposal, proof) {
52450
+ const required = "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0;
52451
+ if (!required) return { required: false, status: "not_required", safe_code: "FRESHNESS_NOT_REQUIRED" };
52452
+ if (!proof) return { required: true, status: "not_checked", safe_code: "FRESHNESS_PROOF_MISSING" };
52453
+ const expired = proof.result === "fresh" && Date.parse(proof.valid_until) < Date.now();
52454
+ return {
52455
+ required: true,
52456
+ status: expired ? "unavailable" : proof.result,
52457
+ safe_code: expired ? "FRESHNESS_PROOF_EXPIRED" : proof.safe_code,
52458
+ checked_at: proof.checked_at,
52459
+ valid_until: proof.valid_until,
52460
+ proof_digest: proof.proof_digest,
52461
+ target_count: proof.target_count,
52462
+ supporting_count: proof.supporting_count,
52463
+ checks: proof.checks
52464
+ };
52465
+ }
52466
+ function freshnessHttpStatus(result) {
52467
+ if (result.status === "fresh" || result.status === "not_required") return 200;
52468
+ if (result.status === "stale") return 409;
52469
+ if (result.status === "unavailable") return 503;
52470
+ return 422;
52471
+ }
51285
52472
  async function executeSafeActionPreview(input) {
51286
52473
  const prepared = await prepareSafeActionPreview({ projectRoot: input.projectRoot, configPath: input.configPath });
51287
52474
  const previewConfigPath = path14.resolve(input.projectRoot, prepared.config_path);
@@ -52301,6 +53488,7 @@ function buildStory(payload) {
52301
53488
  const principalId = (cs.principal && cs.principal.id) || "the agent";
52302
53489
  const requiredRole = (cs.approval && cs.approval.required_role) || "a reviewer";
52303
53490
  const approvalProgress = payload.approval_progress || { approved: 0, required: 1, remaining: 1, complete: false };
53491
+ const freshness = payload.freshness || { required: false, status: "not_required" };
52304
53492
  const story = el("div", { class: "story" });
52305
53493
 
52306
53494
  // 1. Agent requested a change
@@ -52346,8 +53534,17 @@ function buildStory(payload) {
52346
53534
  el("div", { class: "kv" }, [
52347
53535
  el("dt", { text: "Approval progress" }), el("dd", { text: approvalProgress.approved + "/" + approvalProgress.required }),
52348
53536
  el("dt", { text: "Policy result" }), el("dd", { text: (rv.policy_and_risk && rv.policy_and_risk.decision) || stateVal }),
53537
+ el("dt", { text: "Live freshness" }), el("dd", { text: String(freshness.status || "not checked").replaceAll("_", " ") }),
53538
+ el("dt", { text: "Freshness checks" }), el("dd", { text: String(freshness.target_count || 0) + " target / " + String(freshness.supporting_count || 0) + " supporting" }),
52349
53539
  ]),
52350
53540
  ];
53541
+ if (freshness.required) {
53542
+ approveBody.push(el("p", { text: freshness.status === "fresh"
53543
+ ? "The live preflight passed. Approval still does not guarantee freshness through apply; the trusted apply path checks again."
53544
+ : freshness.status === "stale"
53545
+ ? "The target or supporting evidence drifted. This proposal cannot be refreshed; create a new source read and proposal."
53546
+ : "A live source preflight is required before this proposal can be approved." }));
53547
+ }
52351
53548
  const approvedEv = find("proposal_approved");
52352
53549
  const rejectedEv = find("proposal_rejected");
52353
53550
  if (stateVal === "pending_review") {
@@ -52466,10 +53663,42 @@ async function loadDetail(proposalId) {
52466
53663
  actor.setAttribute("aria-label", "Reviewer identity");
52467
53664
  reason.setAttribute("aria-label", "Reason for approval or rejection");
52468
53665
  const actions = el("div", { class: "actions" });
53666
+ const freshness = payload.freshness || { required: false, status: "not_required" };
53667
+ const freshnessStatus = el("div", { class: "status-line", text: freshness.required
53668
+ ? "Freshness: " + String(freshness.status || "not checked").replaceAll("_", " ") + "."
53669
+ : "Freshness: not required for this legacy proposal." });
53670
+ const check = freshness.required
53671
+ ? el("button", { class: "secondary", text: "Check live freshness", onclick: async () => {
53672
+ check.disabled = true;
53673
+ try {
53674
+ await api("/api/proposals/" + encodeURIComponent(proposalId) + "/check-freshness", {
53675
+ method: "POST",
53676
+ headers: { "x-synapsor-csrf": csrfToken },
53677
+ body: JSON.stringify({}),
53678
+ });
53679
+ } catch (error) {
53680
+ freshnessStatus.textContent = error.message;
53681
+ } finally {
53682
+ await loadProposals();
53683
+ await loadDetail(proposalId);
53684
+ }
53685
+ } })
53686
+ : null;
52469
53687
  const approve = el("button", { text: "Approve outside MCP", onclick: async () => { await api("/api/proposals/" + encodeURIComponent(proposalId) + "/approve", { method: "POST", headers: { "x-synapsor-csrf": csrfToken }, body: JSON.stringify({ actor: actor.value, reason: reason.value, confirm: "approve" }) }); await loadProposals(); await loadDetail(proposalId); } });
53688
+ approve.disabled = freshness.required && freshness.status !== "fresh";
53689
+ approve.title = freshness.required
53690
+ ? "A fresh live check is required. Approval performs another check immediately before recording the decision."
53691
+ : "Record this human decision outside MCP.";
52470
53692
  const reject = el("button", { class: "danger", text: "Reject", onclick: async () => { await api("/api/proposals/" + encodeURIComponent(proposalId) + "/reject", { method: "POST", headers: { "x-synapsor-csrf": csrfToken }, body: JSON.stringify({ actor: actor.value, reason: reason.value || "rejected from local UI", confirm: "reject" }) }); await loadProposals(); await loadDetail(proposalId); } });
53693
+ if (check) actions.append(check);
52471
53694
  actions.append(approve, reject);
52472
- reviewPane.append(el("div", { class: "callout", text: "You are the approval authority here \u2014 the model cannot reach these controls." }), actor, reason, actions);
53695
+ reviewPane.append(
53696
+ el("div", { class: "callout", text: "You are the approval authority here \u2014 the model cannot reach these controls." }),
53697
+ freshnessStatus,
53698
+ actor,
53699
+ reason,
53700
+ actions,
53701
+ );
52473
53702
  } else if (proposal.state === "approved" || proposal.state === "pending_worker") {
52474
53703
  const command = trustedApplyCommand(proposalId);
52475
53704
  const commandBox = el("div", { class: "mono", text: command, style: "display:block;margin-top:8px" });
@@ -52495,6 +53724,7 @@ async function loadDetail(proposalId) {
52495
53724
  el("h3", { text: "events", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.events),
52496
53725
  el("h3", { text: "receipts", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.receipts),
52497
53726
  el("h3", { text: "evidence", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.evidence),
53727
+ el("h3", { text: "freshness", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.freshness),
52498
53728
  );
52499
53729
  root.append(reviewPane, jsonPane);
52500
53730
  }
@@ -53159,7 +54389,16 @@ async function createComplianceReport(input) {
53159
54389
  capability: proposal.capability ?? proposal.action,
53160
54390
  principal: approval.approver,
53161
54391
  object: `${proposal.business_object}:${proposal.object_id}`,
53162
- details: sanitize({ status: approval.status, reason: approval.reason, identity_provider: approval.identity?.provider, verified: approval.identity?.verified, subject: approval.identity?.subject, decision_hash: approval.decision_hash, integrity_hash: approval.integrity_hash })
54392
+ details: sanitize({
54393
+ status: approval.status,
54394
+ reason: approval.reason,
54395
+ identity_provider: approval.identity?.provider,
54396
+ verified: approval.identity?.verified,
54397
+ subject: approval.identity?.subject,
54398
+ decision_hash: approval.decision_hash,
54399
+ integrity_hash: approval.integrity_hash,
54400
+ freshness_proof_digest: approval.freshness_proof_digest
54401
+ })
53163
54402
  });
53164
54403
  for (const event of store.events(proposal.proposal_id)) entries.push({
53165
54404
  timestamp: event.created_at,
@@ -53169,7 +54408,7 @@ async function createComplianceReport(input) {
53169
54408
  capability: proposal.capability ?? proposal.action,
53170
54409
  principal: event.actor,
53171
54410
  object: `${proposal.business_object}:${proposal.object_id}`,
53172
- details: sanitize({ kind: event.kind, actor: event.actor, payload_included: false })
54411
+ details: complianceEventDetails(event)
53173
54412
  });
53174
54413
  for (const intent of store.listWritebackIntents({ proposal_id: proposal.proposal_id, limit: 1e6 })) entries.push({
53175
54414
  timestamp: intent.created_at,
@@ -53346,6 +54585,7 @@ async function verifyComplianceReport(report, publicKeyPath) {
53346
54585
  }
53347
54586
  function proposalEntry(proposal) {
53348
54587
  const change = proposal.change_set;
54588
+ const freshness = "freshness" in change && change.freshness ? parseFreshnessAuthority(change.freshness) : void 0;
53349
54589
  return {
53350
54590
  timestamp: proposal.created_at,
53351
54591
  category: "proposal",
@@ -53365,10 +54605,64 @@ function proposalEntry(proposal) {
53365
54605
  after: change.after,
53366
54606
  allowed_columns: change.guards.allowed_columns,
53367
54607
  source_database_mutated: proposal.source_database_mutated,
53368
- proposal_hash: proposal.proposal_hash
54608
+ proposal_hash: proposal.proposal_hash,
54609
+ freshness_required: Boolean(freshness),
54610
+ freshness_authority: freshness ? {
54611
+ schema_version: freshness.schema_version,
54612
+ target_mode: freshness.target.mode,
54613
+ target_member_count: freshness.target.member_count,
54614
+ supporting_dependency_count: freshness.dependencies.length,
54615
+ dependency_ids: freshness.dependencies.map((dependency) => dependency.id),
54616
+ dependency_set_digest: freshness.dependency_set_digest
54617
+ } : void 0
53369
54618
  })
53370
54619
  };
53371
54620
  }
54621
+ function complianceEventDetails(event) {
54622
+ const base = { kind: event.kind, actor: event.actor, payload_included: false };
54623
+ if (event.kind === "proposal_freshness_checked") {
54624
+ try {
54625
+ const proof = parseFreshnessProof(event.payload.proof);
54626
+ return sanitize({
54627
+ ...base,
54628
+ freshness: {
54629
+ schema_version: proof.schema_version,
54630
+ result: proof.result,
54631
+ safe_code: proof.safe_code,
54632
+ checked_at: proof.checked_at,
54633
+ valid_until: proof.valid_until,
54634
+ proof_digest: proof.proof_digest,
54635
+ dependency_set_digest: proof.dependency_set_digest,
54636
+ target_count: proof.target_count,
54637
+ supporting_count: proof.supporting_count,
54638
+ checks: proof.checks
54639
+ }
54640
+ });
54641
+ } catch {
54642
+ return sanitize({ ...base, freshness: { status: "invalid_stored_proof" } });
54643
+ }
54644
+ }
54645
+ if (event.kind === "proposal_approval_blocked_freshness") {
54646
+ return sanitize({
54647
+ ...base,
54648
+ freshness: {
54649
+ result: event.payload.result,
54650
+ safe_code: event.payload.safe_code,
54651
+ proof_digest: event.payload.proof_digest
54652
+ }
54653
+ });
54654
+ }
54655
+ if (event.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(event.payload.safe_error_code ?? ""))) {
54656
+ return sanitize({
54657
+ ...base,
54658
+ freshness: {
54659
+ safe_code: event.payload.safe_error_code,
54660
+ apply_revalidation_blocked: true
54661
+ }
54662
+ });
54663
+ }
54664
+ return sanitize(base);
54665
+ }
53372
54666
  function queryAuditEntry(audit2) {
53373
54667
  const proposalId = string(audit2.proposal_id);
53374
54668
  const objectType = string(audit2.business_object) ?? string(audit2.table_name);
@@ -56571,6 +57865,7 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
56571
57865
  const outbox = stableBy(store.listCloudOutbox({ proposal_id: proposalId, limit: 1e3 }), (item) => `${item.created_at}:${item.event_id}`);
56572
57866
  const governance = stableBy(store.listCloudGovernanceEvents(proposalId), (item) => `${item.created_at}:${item.event_id}`);
56573
57867
  const replay2 = store.getStoredReplayForProposal(proposalId);
57868
+ const latestFreshness = store.latestFreshnessProof(proposalId);
56574
57869
  assertLifecycleLinks(proposal, evidence2, audit2, jobs, intents, receipts2, worker, governance, replay2);
56575
57870
  const changeSet = asRecord4(proposal.change_set);
56576
57871
  if (!changeSet) {
@@ -56589,6 +57884,9 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
56589
57884
  const latestReceipt = receipts2.at(-1);
56590
57885
  const lineage = compensationLineage(changeSet);
56591
57886
  const inverseReceipts = receipts2.map((item) => asRecord4(item.receipt)?.inverse).filter((item) => item !== void 0).map((item) => safeDomainValue(item));
57887
+ const freshnessAuthority = asRecord4(changeSet.freshness);
57888
+ const freshnessTarget = asRecord4(freshnessAuthority?.target);
57889
+ const freshnessDependencies3 = Array.isArray(freshnessAuthority?.dependencies) ? freshnessAuthority.dependencies : [];
56592
57890
  return {
56593
57891
  schema_version: lifecycleViewSchemaVersion,
56594
57892
  selection,
@@ -56635,6 +57933,21 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
56635
57933
  decisions: approvals.map(publicApproval),
56636
57934
  tripped_policy_limits: policyLimitTrips(events2)
56637
57935
  },
57936
+ freshness: {
57937
+ required: freshnessAuthority !== void 0,
57938
+ target_count: latestFreshness?.target_count ?? numberValue(freshnessTarget?.member_count) ?? 0,
57939
+ supporting_count: latestFreshness?.supporting_count ?? freshnessDependencies3.length,
57940
+ latest_status: latestFreshness?.result ?? (freshnessAuthority ? "not_checked" : "not_required"),
57941
+ latest_checked_at: latestFreshness?.checked_at ?? null,
57942
+ latest_proof_digest: latestFreshness?.proof_digest ?? null,
57943
+ latest_safe_code: latestFreshness?.safe_code ?? null,
57944
+ approval_proofs: approvals.map((item) => ({
57945
+ approval_id: item.approval_id,
57946
+ approver: item.approver,
57947
+ proof_digest: item.freshness_proof_digest ?? null
57948
+ })),
57949
+ next_action: latestFreshness?.result === "stale" ? "Create a new source read and proposal." : latestFreshness?.result === "unavailable" ? "Retry the live freshness check when the source is available." : freshnessAuthority && !latestFreshness ? "Run proposals check-freshness before approval." : "Apply still revalidates target and supporting dependencies."
57950
+ },
56638
57951
  evidence: {
56639
57952
  bundles: evidence2.map(publicEvidence),
56640
57953
  count: evidence2.length
@@ -56690,6 +58003,7 @@ function formatLifecycleFirstLook(view) {
56690
58003
  `Object: ${view.proposal.scope.business_object}:${view.proposal.scope.object_id}`,
56691
58004
  `Trusted scope: tenant=${view.proposal.scope.tenant_id} principal=${view.proposal.scope.principal}`,
56692
58005
  `Approval: ${view.approval.status} via ${view.approval.source} (${view.approval.progress.approved}/${view.approval.progress.required})`,
58006
+ `Freshness: ${view.freshness.latest_status}; target=${view.freshness.target_count} supporting=${view.freshness.supporting_count}; proof=${view.freshness.latest_proof_digest ?? "none"}`,
56693
58007
  `Evidence: ${view.evidence.count} bundle${view.evidence.count === 1 ? "" : "s"}; query audit: ${view.query_audit.count}`,
56694
58008
  `Writeback: ${view.writeback.jobs.length ? `${view.writeback.jobs.length} job(s)` : "not created"}; ${view.writeback.intents.length ? `${view.writeback.intents.length} intent(s)` : "no intent"}`,
56695
58009
  `Latest outcome: ${latest ? `${String(latest.status)} rows=${String(latest.rows_affected)} source_changed=${String(latest.source_database_mutated)}` : "not applied"}`,
@@ -56709,6 +58023,7 @@ function formatLifecycleDetails(view) {
56709
58023
  const sections = [
56710
58024
  ["Proposal", view.proposal],
56711
58025
  ["Approval", view.approval],
58026
+ ["Freshness", view.freshness],
56712
58027
  ["Evidence", view.evidence],
56713
58028
  ["Query audit", view.query_audit],
56714
58029
  ["Writeback", view.writeback],
@@ -56827,6 +58142,7 @@ function publicApproval(approval) {
56827
58142
  decision_hash: identity.decision_hash,
56828
58143
  integrity_hash: identity.integrity_hash
56829
58144
  } : null,
58145
+ freshness_proof_digest: approval.freshness_proof_digest ?? null,
56830
58146
  created_at: approval.created_at
56831
58147
  };
56832
58148
  }
@@ -57208,7 +58524,7 @@ function createScopedExploreMcpServer(runtime) {
57208
58524
  }).strict().optional()
57209
58525
  }).strict();
57210
58526
  const server = new McpServer2(
57211
- { name: "synapsor-runner-authoring", version: "1.6.0" },
58527
+ { name: "synapsor-runner-authoring", version: "1.6.1" },
57212
58528
  { capabilities: { tools: {} } }
57213
58529
  );
57214
58530
  server.registerTool(SCOPED_EXPLORE_DESCRIBE_TOOL, {
@@ -61926,6 +63242,29 @@ async function directSqlWritebackDoctorChecks(config, sourceName, source, writeU
61926
63242
  message: `Rollback-only writer probe failed for configured target ${capability.target.schema}.${capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT/${capabilityOperation(capability).toUpperCase()} on the target table and configured columns.`
61927
63243
  });
61928
63244
  }
63245
+ for (const dependency of proposalFreshnessDependencyCapabilities(config, capability)) {
63246
+ try {
63247
+ await rollbackOnlyFreshnessDependencyProbe(
63248
+ source.engine,
63249
+ writeUrl,
63250
+ dependency.capability,
63251
+ dependency.versionColumn
63252
+ );
63253
+ checks.push({
63254
+ name: `capability:${capability.name}:freshness-dependency:${dependency.id}:lock-probe`,
63255
+ ok: true,
63256
+ level: "pass",
63257
+ message: `Rollback-only writer probe verified locking-read authority for freshness dependency ${dependency.capability.target.schema}.${dependency.capability.target.table} without reading or mutating business rows.`
63258
+ });
63259
+ } catch (error) {
63260
+ checks.push({
63261
+ name: `capability:${capability.name}:freshness-dependency:${dependency.id}:lock-probe`,
63262
+ ok: false,
63263
+ level: "fail",
63264
+ message: `Rollback-only locking-read probe failed for freshness dependency ${dependency.capability.target.schema}.${dependency.capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT plus row-lock authority on the dependency relation; a narrow UPDATE grant on its version column is sufficient for the supported PostgreSQL/MySQL fixtures.`
63265
+ });
63266
+ }
63267
+ }
61929
63268
  }
61930
63269
  return checks;
61931
63270
  }
@@ -61935,6 +63274,22 @@ function directSqlProposalCapabilities(config, sourceName) {
61935
63274
  return capabilityWritebackMode2(capability) === "direct_sql";
61936
63275
  });
61937
63276
  }
63277
+ function proposalFreshnessDependencyCapabilities(config, proposal) {
63278
+ const policy = config.proposal_freshness?.[proposal.name];
63279
+ if (!policy) return [];
63280
+ const capabilities = new Map((config.capabilities ?? []).map((capability) => [capability.name, capability]));
63281
+ return (policy.dependencies ?? []).map((dependency) => {
63282
+ const capability = capabilities.get(dependency.capability);
63283
+ if (!capability) {
63284
+ throw new Error(`reviewed freshness dependency capability is missing: ${dependency.capability}`);
63285
+ }
63286
+ return {
63287
+ id: dependency.id,
63288
+ capability,
63289
+ versionColumn: dependency.version_column
63290
+ };
63291
+ });
63292
+ }
61938
63293
  async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
61939
63294
  if (engine === "postgres") {
61940
63295
  await rollbackOnlyPostgresTargetProbe(databaseUrl, capability);
@@ -61942,6 +63297,45 @@ async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
61942
63297
  }
61943
63298
  await rollbackOnlyMysqlTargetProbe(databaseUrl, capability);
61944
63299
  }
63300
+ async function rollbackOnlyFreshnessDependencyProbe(engine, databaseUrl, capability, versionColumn) {
63301
+ if (engine === "postgres") {
63302
+ const pg = await dynamicImportModule("pg");
63303
+ const pool = new pg.Pool({ connectionString: databaseUrl });
63304
+ const client = await pool.connect();
63305
+ try {
63306
+ await client.query("BEGIN");
63307
+ try {
63308
+ const table = `${quotePostgresIdentifier3(capability.target.schema)}.${quotePostgresIdentifier3(capability.target.table)}`;
63309
+ const columns = freshnessDependencyProbeColumns(capability, versionColumn).map(quotePostgresIdentifier3).join(", ");
63310
+ await client.query(`SELECT ${columns} FROM ${table} WHERE false FOR UPDATE`);
63311
+ await client.query("ROLLBACK");
63312
+ } catch (error) {
63313
+ await client.query("ROLLBACK").catch(() => void 0);
63314
+ throw error;
63315
+ }
63316
+ } finally {
63317
+ client.release();
63318
+ await pool.end();
63319
+ }
63320
+ return;
63321
+ }
63322
+ const mysql5 = await dynamicImportModule("mysql2/promise");
63323
+ const connection = await mysql5.createConnection({ uri: databaseUrl, dateStrings: true });
63324
+ try {
63325
+ await connection.beginTransaction();
63326
+ try {
63327
+ const table = `${quoteMysqlIdentifier2(capability.target.schema)}.${quoteMysqlIdentifier2(capability.target.table)}`;
63328
+ const columns = freshnessDependencyProbeColumns(capability, versionColumn).map(quoteMysqlIdentifier2).join(", ");
63329
+ await connection.query(`SELECT ${columns} FROM ${table} WHERE 1 = 0 FOR UPDATE`);
63330
+ await connection.rollback();
63331
+ } catch (error) {
63332
+ await connection.rollback().catch(() => void 0);
63333
+ throw error;
63334
+ }
63335
+ } finally {
63336
+ await connection.end();
63337
+ }
63338
+ }
61945
63339
  async function rollbackOnlyPostgresTargetProbe(databaseUrl, capability) {
61946
63340
  const pg = await dynamicImportModule("pg");
61947
63341
  const pool = new pg.Pool({ connectionString: databaseUrl });
@@ -62024,6 +63418,14 @@ function proposalReadProbeColumns(capability) {
62024
63418
  }
62025
63419
  return [...columns];
62026
63420
  }
63421
+ function freshnessDependencyProbeColumns(capability, versionColumn) {
63422
+ return [...new Set([
63423
+ capability.target.primary_key,
63424
+ capability.target.tenant_key,
63425
+ capability.target.principal_scope_key,
63426
+ versionColumn
63427
+ ].filter((column) => Boolean(column)))];
63428
+ }
62027
63429
  function proposalWriteProbeColumns(capability) {
62028
63430
  const columns = /* @__PURE__ */ new Set();
62029
63431
  for (const column of capability.allowed_columns ?? []) columns.add(column);
@@ -63226,6 +64628,11 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath, options
63226
64628
  if (proposal.proposal_hash !== job.approval_id) {
63227
64629
  throw new Error("writeback approval/proposal digest does not match local proposal");
63228
64630
  }
64631
+ const proposalFreshness = "freshness" in proposal.change_set && proposal.change_set.freshness ? parseFreshnessAuthority(proposal.change_set.freshness) : void 0;
64632
+ const jobFreshness = "freshness" in job && job.freshness ? parseFreshnessAuthority(job.freshness) : void 0;
64633
+ if (Boolean(proposalFreshness) !== Boolean(jobFreshness) || proposalFreshness && jobFreshness && proposalFreshness.dependency_set_digest !== jobFreshness.dependency_set_digest) {
64634
+ throw new Error("writeback freshness authority does not match the immutable local proposal");
64635
+ }
63229
64636
  const proposalContract = proposal.change_set.contract;
63230
64637
  const reviewedContract = matching.contract_provenance;
63231
64638
  if (reviewedContract) {
@@ -63259,12 +64666,42 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath, options
63259
64666
  } else if (proposalPrincipalScope) {
63260
64667
  throw new Error("local proposal carries principal scope outside the reviewed capability");
63261
64668
  }
63262
- if (!options.cloudApproved) await verifyStoredApprovalAuthority(config, configPath, store, proposal, matching);
64669
+ if (!options.cloudApproved) {
64670
+ verifyStoredFreshnessApprovalAuthority(store, proposal);
64671
+ await verifyStoredApprovalAuthority(config, configPath, store, proposal, matching);
64672
+ }
63263
64673
  } finally {
63264
64674
  store.close();
63265
64675
  }
63266
64676
  }
63267
64677
  }
64678
+ function verifyStoredFreshnessApprovalAuthority(store, proposal) {
64679
+ if (!("freshness" in proposal.change_set) || !proposal.change_set.freshness) return;
64680
+ const authority = parseFreshnessAuthority(proposal.change_set.freshness);
64681
+ const approvals = store.approvals(proposal.proposal_id).filter((approval) => approval.status === "approved" && approval.proposal_hash === proposal.proposal_hash && approval.proposal_version === proposal.proposal_version);
64682
+ const required = proposal.change_set.approval.required_approvals ?? 1;
64683
+ if (new Set(approvals.map((approval) => approval.approver)).size < required) {
64684
+ throw new Error(`proposal ${proposal.proposal_id} does not have its required freshness-bound approvals`);
64685
+ }
64686
+ const proofEvents = store.events(proposal.proposal_id).filter((event) => event.kind === "proposal_freshness_checked");
64687
+ for (const approval of approvals) {
64688
+ if (!approval.freshness_proof_digest) {
64689
+ throw new Error(`proposal ${proposal.proposal_id} has an approval without a freshness proof`);
64690
+ }
64691
+ const event = proofEvents.find((candidate) => {
64692
+ try {
64693
+ return parseFreshnessProof(candidate.payload.proof).proof_digest === approval.freshness_proof_digest;
64694
+ } catch {
64695
+ return false;
64696
+ }
64697
+ });
64698
+ if (!event) throw new Error(`proposal ${proposal.proposal_id} freshness proof is missing or failed integrity validation`);
64699
+ const proof = parseFreshnessProof(event.payload.proof);
64700
+ if (proof.result !== "fresh" || proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
64701
+ throw new Error(`proposal ${proposal.proposal_id} approval freshness proof does not match immutable authority`);
64702
+ }
64703
+ }
64704
+ }
63268
64705
  async function verifyStoredApprovalAuthority(config, configPath, store, proposal, capability) {
63269
64706
  if (!config.operator_identity || config.operator_identity.provider === "dev_env") return;
63270
64707
  const approval = [...store.approvals(proposal.proposal_id)].reverse().find((item) => item.status === "approved" && item.proposal_hash === proposal.proposal_hash && item.proposal_version === proposal.proposal_version);
@@ -67082,6 +68519,7 @@ async function proposals(args) {
67082
68519
  const [subcommand, ...rest] = args;
67083
68520
  if (subcommand === "list") return proposalsList(rest);
67084
68521
  if (subcommand === "show") return proposalsShow(rest);
68522
+ if (subcommand === "check-freshness") return proposalsCheckFreshness(rest);
67085
68523
  if (subcommand === "approve") return proposalsApprove(rest);
67086
68524
  if (subcommand === "reject") return proposalsReject(rest);
67087
68525
  if (subcommand === "writeback-job") return proposalsWritebackJob(rest);
@@ -68043,13 +69481,21 @@ async function proposalsShow(args) {
68043
69481
  if (!proposal) throw new Error(`proposal not found: ${resolvedProposalId}`);
68044
69482
  const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
68045
69483
  const approvalProgress = store.approvalProgress(resolvedProposalId);
68046
- const payload = { proposal, approval_progress: approvalProgress, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId), evidence: evidence2 };
69484
+ const latestFreshness = store.latestFreshnessProof(resolvedProposalId);
69485
+ const freshness = {
69486
+ required: "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0,
69487
+ status: latestFreshness?.result ?? ("freshness" in proposal.change_set && proposal.change_set.freshness !== void 0 ? "not_checked" : "not_required"),
69488
+ proof: latestFreshness ?? null
69489
+ };
69490
+ const payload = { proposal, approval_progress: approvalProgress, freshness, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId), evidence: evidence2 };
68047
69491
  if (args.includes("--json")) {
68048
69492
  process2.stdout.write(`${JSON.stringify(payload, null, 2)}
68049
69493
  `);
68050
69494
  } else if (showDetails(args)) {
68051
69495
  process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
68052
69496
  process2.stdout.write(`Approval progress: ${approvalProgress.approved}/${approvalProgress.required}${approvalProgress.rejected ? " (rejected)" : ""}
69497
+ `);
69498
+ process2.stdout.write(`Freshness: ${freshness.status}${latestFreshness ? ` checked=${latestFreshness.checked_at} proof=${latestFreshness.proof_digest}` : ""}
68053
69499
  `);
68054
69500
  process2.stdout.write(formatProposalEventDetail(payload.events));
68055
69501
  if (args.includes("--debug")) process2.stdout.write(formatProposalDebug(proposal, optionalArg(args, "--store")));
@@ -68061,6 +69507,119 @@ async function proposalsShow(args) {
68061
69507
  store.close();
68062
69508
  }
68063
69509
  }
69510
+ var freshnessExitCodes = {
69511
+ fresh: 0,
69512
+ not_required: 0,
69513
+ stale: 3,
69514
+ unavailable: 4,
69515
+ invalid: 5,
69516
+ unsupported: 6
69517
+ };
69518
+ async function proposalsCheckFreshness(args) {
69519
+ const proposalId = positional(args, 0);
69520
+ if (!proposalId) throw new Error("proposals check-freshness requires <proposal_id|latest>");
69521
+ const storePath = localStorePath(args);
69522
+ const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
69523
+ const config = await optionalRuntimeConfig(configPath);
69524
+ if (config && runtimeStoreBridgeRequired(args, config)) {
69525
+ return withSharedPostgresRuntimeStoreBridge(
69526
+ args,
69527
+ config,
69528
+ `proposals check-freshness ${proposalId}`,
69529
+ (bridgeStorePath) => proposalsCheckFreshness(argsWithRuntimeStoreBridge(args, bridgeStorePath))
69530
+ );
69531
+ }
69532
+ assertNoRuntimeStoreForLocalMutation(config, "proposals check-freshness", args);
69533
+ if (sharedPostgresLedgerMirrorRequested(args, config)) {
69534
+ return withSharedPostgresLedgerMirror(
69535
+ args,
69536
+ storePath,
69537
+ `proposals check-freshness ${proposalId}`,
69538
+ () => proposalsCheckFreshness(withoutSharedPostgresLedgerMirror(args)),
69539
+ config
69540
+ );
69541
+ }
69542
+ const store = await openLocalStore(args);
69543
+ try {
69544
+ const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
69545
+ const proposal = requireLocalProposal(store, resolvedProposalId);
69546
+ const freshness = await evaluateAndRecordProposalFreshness({ proposal, config, configPath, store });
69547
+ operationalLog(freshness.status === "fresh" || freshness.status === "not_required" ? "info" : "warn", "proposal_freshness_check", {
69548
+ proposal_id: proposal.proposal_id,
69549
+ capability: proposal.action,
69550
+ tenant: proposal.tenant_id,
69551
+ status: freshness.status,
69552
+ error_code: freshness.safe_code,
69553
+ target_count: freshness.target_count,
69554
+ supporting_count: freshness.supporting_count,
69555
+ ...freshness.required ? { freshness_proof_digest: freshness.proof.proof_digest } : {},
69556
+ source_database_changed: false
69557
+ });
69558
+ process2.stdout.write(args.includes("--json") ? `${JSON.stringify(freshnessJson(freshness), null, 2)}
69559
+ ` : formatFreshnessResult(freshness, args.includes("--details")));
69560
+ return freshnessExitCodes[freshness.status];
69561
+ } finally {
69562
+ store.close();
69563
+ }
69564
+ }
69565
+ async function evaluateAndRecordProposalFreshness(input) {
69566
+ const required = "freshness" in input.proposal.change_set && input.proposal.change_set.freshness !== void 0;
69567
+ if (!required) {
69568
+ return {
69569
+ required: false,
69570
+ status: "not_required",
69571
+ safe_code: "FRESHNESS_NOT_REQUIRED",
69572
+ target_count: 0,
69573
+ supporting_count: 0
69574
+ };
69575
+ }
69576
+ if (!input.config) {
69577
+ throw new Error(`freshness-required proposal needs an existing --config file; not found: ${path25.resolve(input.configPath)}`);
69578
+ }
69579
+ const result = await evaluateProposalFreshness({
69580
+ config: input.config,
69581
+ proposal: input.proposal,
69582
+ env: process2.env
69583
+ });
69584
+ if (result.required) input.store.recordFreshnessProof(result.proof);
69585
+ return result;
69586
+ }
69587
+ function freshnessJson(result) {
69588
+ return {
69589
+ schema_version: "synapsor.proposal-freshness-result.v1",
69590
+ required: result.required,
69591
+ status: result.status,
69592
+ safe_code: result.safe_code,
69593
+ target_count: result.target_count,
69594
+ supporting_count: result.supporting_count,
69595
+ ...result.required ? { proof: result.proof } : {},
69596
+ source_database_changed: false
69597
+ };
69598
+ }
69599
+ function formatFreshnessResult(result, details) {
69600
+ if (!result.required) return "Freshness: not required (legacy target guard remains enforced at apply).\n";
69601
+ const lines = [
69602
+ `Freshness: ${result.status}`,
69603
+ `code: ${result.safe_code}`,
69604
+ `target checks: ${result.target_count}`,
69605
+ `supporting checks: ${result.supporting_count}`,
69606
+ `checked at: ${result.proof.checked_at}`,
69607
+ `proof: ${result.proof.proof_digest}`
69608
+ ];
69609
+ if (result.status === "stale") lines.push("next: create a new source read and proposal; this proposal cannot be refreshed or approved");
69610
+ else if (result.status === "unavailable") lines.push("next: retry the live check after the source is available; no approval was recorded");
69611
+ else if (result.status === "invalid" || result.status === "unsupported") lines.push("next: fix the reviewed freshness configuration; no approval was recorded");
69612
+ else lines.push("note: approval-time freshness does not replace the final apply-time revalidation");
69613
+ if (details) {
69614
+ for (const check of result.proof.checks) {
69615
+ lines.push(
69616
+ `- ${check.kind}:${check.id} ${check.status} (${check.safe_code})${check.expected_version_digest ? ` expected=${check.expected_version_digest}` : ""}${check.observed_version_digest ? ` observed=${check.observed_version_digest}` : ""}`
69617
+ );
69618
+ }
69619
+ }
69620
+ return `${lines.join("\n")}
69621
+ `;
69622
+ }
68064
69623
  async function proposalsApprove(args) {
68065
69624
  const proposalId = positional(args, 0);
68066
69625
  if (!proposalId) throw new Error("proposals approve requires <proposal_id>");
@@ -68083,6 +69642,20 @@ async function proposalsApprove(args) {
68083
69642
  const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
68084
69643
  process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
68085
69644
  }
69645
+ const freshness = await evaluateAndRecordProposalFreshness({ proposal, config, configPath, store });
69646
+ if (!args.includes("--json")) process2.stdout.write(formatFreshnessResult(freshness, args.includes("--details")));
69647
+ if (freshness.status !== "fresh" && freshness.status !== "not_required") {
69648
+ if (freshness.required) {
69649
+ store.recordFreshnessApprovalBlocked(resolvedProposalId, {
69650
+ proof_digest: freshness.proof.proof_digest,
69651
+ safe_code: freshness.safe_code,
69652
+ actor: "operator"
69653
+ });
69654
+ }
69655
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(freshnessJson(freshness), null, 2)}
69656
+ `);
69657
+ return freshnessExitCodes[freshness.status];
69658
+ }
68086
69659
  await confirmDangerousAction(args, `Approve proposal ${resolvedProposalId} for guarded writeback?`);
68087
69660
  const identity = await operatorIdentityForDecision({ args, config, configPath, proposal, action: "approve", reason: optionalArg(args, "--reason") });
68088
69661
  const updated = store.approveProposal(resolvedProposalId, {
@@ -68091,7 +69664,8 @@ async function proposalsApprove(args) {
68091
69664
  proposal_version: proposal.proposal_version,
68092
69665
  reason: optionalArg(args, "--reason") ?? void 0,
68093
69666
  identity,
68094
- require_verified_identity: Boolean(config?.operator_identity && config.operator_identity.provider !== "dev_env")
69667
+ require_verified_identity: Boolean(config?.operator_identity && config.operator_identity.provider !== "dev_env"),
69668
+ freshness_proof_digest: freshness.required ? freshness.proof.proof_digest : void 0
68095
69669
  });
68096
69670
  operationalLog("info", "operator_decision", {
68097
69671
  action: "approve",
@@ -68102,10 +69676,16 @@ async function proposalsApprove(args) {
68102
69676
  identity_provider: identity.provider,
68103
69677
  identity_verified: identity.verified,
68104
69678
  required_role: proposal.change_set.approval.required_role,
68105
- approval_progress: `${store.approvalProgress(resolvedProposalId).approved}/${store.approvalProgress(resolvedProposalId).required}`
69679
+ approval_progress: `${store.approvalProgress(resolvedProposalId).approved}/${store.approvalProgress(resolvedProposalId).required}`,
69680
+ freshness_status: freshness.status,
69681
+ ...freshness.required ? { freshness_proof_digest: freshness.proof.proof_digest } : {}
68106
69682
  });
68107
69683
  const progress = store.approvalProgress(resolvedProposalId);
68108
- const approvalResult = { ...updated, approval_progress: progress };
69684
+ const approvalResult = {
69685
+ ...updated,
69686
+ approval_progress: progress,
69687
+ freshness: freshnessJson(freshness)
69688
+ };
68109
69689
  process2.stdout.write(args.includes("--json") ? `${JSON.stringify(approvalResult, null, 2)}
68110
69690
  ` : progress.complete ? `approved ${updated.proposal_id} (${progress.approved}/${progress.required})
68111
69691
  ` : `approval recorded for ${updated.proposal_id} (${progress.approved}/${progress.required}); awaiting ${progress.remaining} more verified reviewer${progress.remaining === 1 ? "" : "s"}
@@ -71978,11 +73558,13 @@ Without --config, doctor is the legacy Cloud worker check and requires SYNAPSOR_
71978
73558
  ${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
71979
73559
  ${cmd} proposals show latest
71980
73560
  ${cmd} proposals show latest --details
73561
+ ${cmd} proposals check-freshness latest --config ./synapsor.runner.json --store ./.synapsor/local.db
73562
+ ${cmd} proposals check-freshness latest --details --json --config ./synapsor.runner.json --store ./.synapsor/local.db
71981
73563
  ${cmd} proposals approve latest --yes
71982
73564
  ${cmd} proposals reject latest --reason "..."
71983
73565
  ${cmd} proposals approve latest --yes --shared-ledger-mirror --shared-ledger-url-env SYNAPSOR_LEDGER_DATABASE_URL
71984
73566
 
71985
- Review decisions happen outside the model-facing MCP tool surface. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
73567
+ Review decisions happen outside the model-facing MCP tool surface. A freshness-required approval performs the same live check automatically and binds the approval to its immutable proof. Freshness at approval improves review quality; apply rechecks again before mutation. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
71986
73568
  `,
71987
73569
  lifecycle: `Usage:
71988
73570
  ${cmd} lifecycle [--store ./.synapsor/local.db]