release-skill 0.9.32 → 0.9.34

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 (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.qoder-plugin/plugin.json +1 -1
  7. package/CHANGELOG.md +54 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +31 -13
  11. package/README.zh-CN.md +17 -13
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +5855 -4045
  14. package/adapters/claude/schemas/.render-manifest.json +8 -4
  15. package/adapters/claude/schemas/release-plan.schema.json +85 -0
  16. package/adapters/claude/schemas/release-project.schema.json +18 -0
  17. package/adapters/claude/schemas/test-intake.schema.json +217 -0
  18. package/adapters/claude/skills/release-assess/SKILL.md +11 -0
  19. package/adapters/claude/skills/release-config/SKILL.md +6 -0
  20. package/adapters/claude/skills/release-help/SKILL.md +20 -4
  21. package/adapters/claude/skills/release-prepare/SKILL.md +11 -2
  22. package/adapters/claude/skills/release-setup/SKILL.md +15 -2
  23. package/adapters/claude/skills/release-verify/SKILL.md +2 -0
  24. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  25. package/adapters/codex/bin/release-skill.bundle.mjs +5855 -4045
  26. package/adapters/codex/schemas/.render-manifest.json +8 -4
  27. package/adapters/codex/schemas/release-plan.schema.json +85 -0
  28. package/adapters/codex/schemas/release-project.schema.json +18 -0
  29. package/adapters/codex/schemas/test-intake.schema.json +217 -0
  30. package/adapters/codex/skills/release-assess/SKILL.md +11 -0
  31. package/adapters/codex/skills/release-config/SKILL.md +6 -0
  32. package/adapters/codex/skills/release-help/SKILL.md +20 -4
  33. package/adapters/codex/skills/release-prepare/SKILL.md +11 -2
  34. package/adapters/codex/skills/release-setup/SKILL.md +15 -2
  35. package/adapters/codex/skills/release-verify/SKILL.md +2 -0
  36. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  37. package/adapters/cursor/bin/release-skill.bundle.mjs +5855 -4045
  38. package/adapters/cursor/schemas/.render-manifest.json +8 -4
  39. package/adapters/cursor/schemas/release-plan.schema.json +85 -0
  40. package/adapters/cursor/schemas/release-project.schema.json +18 -0
  41. package/adapters/cursor/schemas/test-intake.schema.json +217 -0
  42. package/adapters/cursor/skills/release-assess/SKILL.md +11 -0
  43. package/adapters/cursor/skills/release-config/SKILL.md +6 -0
  44. package/adapters/cursor/skills/release-help/SKILL.md +20 -4
  45. package/adapters/cursor/skills/release-prepare/SKILL.md +11 -2
  46. package/adapters/cursor/skills/release-setup/SKILL.md +15 -2
  47. package/adapters/cursor/skills/release-verify/SKILL.md +2 -0
  48. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  49. package/adapters/kimi/bin/release-skill.bundle.mjs +5855 -4045
  50. package/adapters/kimi/schemas/.render-manifest.json +8 -4
  51. package/adapters/kimi/schemas/release-plan.schema.json +85 -0
  52. package/adapters/kimi/schemas/release-project.schema.json +18 -0
  53. package/adapters/kimi/schemas/test-intake.schema.json +217 -0
  54. package/adapters/kimi/skills/release-assess/SKILL.md +11 -0
  55. package/adapters/kimi/skills/release-config/SKILL.md +6 -0
  56. package/adapters/kimi/skills/release-help/SKILL.md +20 -4
  57. package/adapters/kimi/skills/release-prepare/SKILL.md +11 -2
  58. package/adapters/kimi/skills/release-setup/SKILL.md +15 -2
  59. package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
  60. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  61. package/adapters/qoder/bin/release-skill.bundle.mjs +5855 -4045
  62. package/adapters/qoder/schemas/.render-manifest.json +8 -4
  63. package/adapters/qoder/schemas/release-plan.schema.json +85 -0
  64. package/adapters/qoder/schemas/release-project.schema.json +18 -0
  65. package/adapters/qoder/schemas/test-intake.schema.json +217 -0
  66. package/adapters/qoder/skills/release-assess/SKILL.md +11 -0
  67. package/adapters/qoder/skills/release-config/SKILL.md +6 -0
  68. package/adapters/qoder/skills/release-help/SKILL.md +20 -4
  69. package/adapters/qoder/skills/release-prepare/SKILL.md +11 -2
  70. package/adapters/qoder/skills/release-setup/SKILL.md +15 -2
  71. package/adapters/qoder/skills/release-verify/SKILL.md +2 -0
  72. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  73. package/adapters/workbuddy/bin/release-skill.bundle.mjs +5855 -4045
  74. package/adapters/workbuddy/schemas/.render-manifest.json +8 -4
  75. package/adapters/workbuddy/schemas/release-plan.schema.json +85 -0
  76. package/adapters/workbuddy/schemas/release-project.schema.json +18 -0
  77. package/adapters/workbuddy/schemas/test-intake.schema.json +217 -0
  78. package/adapters/workbuddy/skills/release-assess/SKILL.md +11 -0
  79. package/adapters/workbuddy/skills/release-config/SKILL.md +6 -0
  80. package/adapters/workbuddy/skills/release-help/SKILL.md +20 -4
  81. package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -2
  82. package/adapters/workbuddy/skills/release-setup/SKILL.md +15 -2
  83. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
  84. package/bin/release-skill-cli.mjs +37 -2
  85. package/bin/release-skill.bundle.mjs +5855 -4045
  86. package/package.json +1 -1
  87. package/platform-manifest.json +4 -4
  88. package/references/.render-manifest.json +6 -6
  89. package/references/01-state-machine.md +12 -1
  90. package/references/02-project-config.md +20 -0
  91. package/references/05-evidence-and-errors.md +30 -0
  92. package/schemas/.render-manifest.json +8 -4
  93. package/schemas/release-plan.schema.json +85 -0
  94. package/schemas/release-project.schema.json +18 -0
  95. package/schemas/test-intake.schema.json +217 -0
  96. package/skills/release-assess/SKILL.md +11 -0
  97. package/skills/release-config/SKILL.md +6 -0
  98. package/skills/release-help/SKILL.md +20 -4
  99. package/skills/release-prepare/SKILL.md +11 -2
  100. package/skills/release-setup/SKILL.md +15 -2
  101. package/skills/release-verify/SKILL.md +2 -0
  102. package/skills-src/release-assess/SKILL.md +11 -0
  103. package/skills-src/release-config/SKILL.md +6 -0
  104. package/skills-src/release-help/SKILL.md +20 -4
  105. package/skills-src/release-prepare/SKILL.md +11 -2
  106. package/skills-src/release-setup/SKILL.md +15 -2
  107. package/skills-src/release-verify/SKILL.md +2 -0
  108. package/src/commands/approve.mjs +13 -0
  109. package/src/commands/prepare.mjs +501 -5
  110. package/src/commands/publish.mjs +2 -0
  111. package/src/commands/reconcile.mjs +2 -0
  112. package/src/commands/route.mjs +32 -7
  113. package/src/commands/setup.mjs +13 -6
  114. package/src/commands/ship.mjs +168 -5
  115. package/src/core/config.mjs +5 -0
  116. package/src/core/plan.mjs +3 -1
  117. package/src/core/test-candidate.mjs +649 -0
  118. package/src/core/test-intake.mjs +645 -0
@@ -3190,15 +3190,22 @@ export async function assessAdoption({
3190
3190
  severity: 'blocking',
3191
3191
  }));
3192
3192
  }
3193
+ // requiredPublicFiles are string targets covered by publicFiles[].to.
3194
+ const publicFromByTo = new Map(
3195
+ publicFileMappings
3196
+ .filter((mapping) => typeof mapping?.to === 'string' && typeof mapping?.from === 'string')
3197
+ .map((mapping) => [mapping.to, mapping.from]),
3198
+ );
3193
3199
  for (const required of unit.requiredPublicFiles ?? []) {
3194
- if (typeof required?.from !== 'string') continue;
3195
- if (await adoptionPathExists(resolve(rootReal, required.from))) continue;
3200
+ if (typeof required !== 'string') continue;
3201
+ const from = publicFromByTo.get(required);
3202
+ if (typeof from === 'string' && await adoptionPathExists(resolve(rootReal, from))) continue;
3196
3203
  findings.push(createFinding({
3197
3204
  category: FINDING_CATEGORY.MANDATORY_GAP, code: 'REQUIRED_PUBLIC_FILE_MISSING',
3198
- unitId, fieldPath: 'releaseUnits[].requiredPublicFiles[].from',
3199
- message: `发布单元 "${unitId}" 的必选公开文件输入不存在: ${required.from}`,
3200
- evidence: { from: required.from },
3201
- action: '创建该文件,或修正 releaseUnits[].requiredPublicFiles[].from。',
3205
+ unitId, fieldPath: 'releaseUnits[].requiredPublicFiles',
3206
+ message: `发布单元 "${unitId}" 的必选公开文件不存在: ${required}`,
3207
+ evidence: { required, from: from ?? null, to: required },
3208
+ action: '创建该文件,或修正 releaseUnits[].requiredPublicFiles 与对应 publicFiles 映射。',
3202
3209
  severity: 'blocking',
3203
3210
  }));
3204
3211
  }
@@ -1,4 +1,4 @@
1
- import { readFile, lstat, mkdir } from 'node:fs/promises';
1
+ import { readFile, lstat, mkdir, realpath } from 'node:fs/promises';
2
2
  import { basename, dirname, resolve } from 'node:path';
3
3
  import { publishFileOrReplace } from 'skill-family-harness-node';
4
4
 
@@ -21,6 +21,10 @@ import {
21
21
  PLAN_DIGEST_MISMATCH,
22
22
  CONSUMER_VERIFICATION_DEFERRED,
23
23
  } from '../core/errors.mjs';
24
+ import {
25
+ applyShipPrepareOutcome,
26
+ resolveShipCandidateContinuation,
27
+ } from '../core/test-candidate.mjs';
24
28
 
25
29
  function normalizeNewShipUnitIds(releaseUnits, requestedUnitIds) {
26
30
  if (requestedUnitIds === undefined) return undefined;
@@ -102,6 +106,66 @@ async function readFrozenPlanForUnitScope(planPath, statePath, stateUnitIds) {
102
106
  }
103
107
  }
104
108
 
109
+ async function sameAuthorityFile(left, right) {
110
+ const identity = async (value) => {
111
+ const absolute = resolve(value);
112
+ try {
113
+ return await realpath(absolute);
114
+ } catch {
115
+ return absolute;
116
+ }
117
+ };
118
+ return (await identity(left)) === (await identity(right));
119
+ }
120
+
121
+ /**
122
+ * A VERIFIED reattach keeps the release fact. An observed postverify is
123
+ * carried with its verify parent so open work can re-enter postVerifyRelease
124
+ * and completed work can feed the existing local finish checklist.
125
+ */
126
+ async function observedPostverifyContinuation(root, planPath) {
127
+ if (!planPath) return null;
128
+ let routing;
129
+ try {
130
+ const { readRunRouting } = await import('./route.mjs');
131
+ routing = await readRunRouting(root, {});
132
+ } catch {
133
+ return null;
134
+ }
135
+ const formal = resolve(planPath);
136
+ const bound = [];
137
+ for (const run of routing?.observedRuns ?? []) {
138
+ if (!run?.planPath || !run.status || !run.runPath || !run.command) continue;
139
+ if (await sameAuthorityFile(run.planPath, formal)) bound.push(run);
140
+ }
141
+ const postverifyRuns = bound.filter((run) => run.command === 'postverify');
142
+ const completed = postverifyRuns.filter((run) => run.status === 'DISTRIBUTED');
143
+ const open = postverifyRuns.filter((run) => run.status !== 'DISTRIBUTED');
144
+ // A completed authority supersedes open attempts for the same verify
145
+ // parent. Multiple completed records are ambiguous; never fall back to an
146
+ // older open attempt or choose by directory order or timestamp.
147
+ if (completed.length > 1) return null;
148
+ const pending = completed.length === 1 ? completed[0] : open.length === 1 ? open[0] : null;
149
+ if (!pending) return null;
150
+ let verify = null;
151
+ for (const run of bound) {
152
+ if (run.command !== 'verify' || run.status !== 'VERIFIED' || !run.runPath) continue;
153
+ if (pending.sourceRunPath && await sameAuthorityFile(run.runPath, pending.sourceRunPath)) {
154
+ verify = run;
155
+ break;
156
+ }
157
+ }
158
+ if (!verify) return null;
159
+ return {
160
+ verifyRunPath: verify.runPath,
161
+ ...(verify.approvalPath ? { approvalPath: verify.approvalPath } : {}),
162
+ postVerify: {
163
+ status: pending.status,
164
+ runPath: pending.runPath,
165
+ },
166
+ };
167
+ }
168
+
105
169
  async function writeJsonAtomic(path, value) {
106
170
  await mkdir(dirname(path), { recursive: true });
107
171
  const { stateDigest: _oldDigest, ...body } = value;
@@ -177,9 +241,10 @@ async function defaultDependencies() {
177
241
 
178
242
  function publicState(state, { postRelease } = {}) {
179
243
  const visiblePostRelease = postRelease === undefined ? state.postRelease : postRelease;
244
+ const showsTestCandidate = state.status === 'NEW' && state.testCandidate;
180
245
  return {
181
246
  command: 'ship',
182
- status: state.status,
247
+ status: showsTestCandidate ? 'NEEDS_INPUT' : state.status,
183
248
  statePath: state.statePath,
184
249
  targetVersion: state.targetVersion,
185
250
  ...(state.hooks && state.hooks.length > 0 ? {
@@ -199,6 +264,10 @@ function publicState(state, { postRelease } = {}) {
199
264
  ...(state.distributeRunPath ? { distributeRunPath: state.distributeRunPath } : {}),
200
265
  ...(state.postVerify ? { postVerify: state.postVerify } : {}),
201
266
  ...(state.requirements ? { requirements: state.requirements } : {}),
267
+ ...(showsTestCandidate ? { testCandidate: state.testCandidate } : {}),
268
+ ...(showsTestCandidate && state.pendingTestRequirements
269
+ ? { pendingTestRequirements: state.pendingTestRequirements }
270
+ : {}),
202
271
  ...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
203
272
  ...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
204
273
  ...(visiblePostRelease ? { postRelease: visiblePostRelease } : {}),
@@ -483,7 +552,77 @@ export async function advanceShip(options = {}, injected = {}) {
483
552
  });
484
553
  }
485
554
 
486
- if (state.status === 'NEW') {
555
+ if (state.status === 'NEW' && state.testCandidate) {
556
+ const loaded = await deps.loadProjectConfig({ root });
557
+ const continuation = resolveShipCandidateContinuation({
558
+ state,
559
+ loadedConfigDigest: loaded.configDigest,
560
+ requestedUnitIds: options.unitIds,
561
+ });
562
+ if (continuation.kind === 'conflict') {
563
+ throw new ReleaseError(GATE_FAILED, `ship test candidate conflict: ${continuation.reason}`, {
564
+ reason: continuation.reason,
565
+ });
566
+ }
567
+ const prepared = await deps.prepareRelease({
568
+ root,
569
+ candidatePath: continuation.candidatePath,
570
+ expectedCandidateSha256: continuation.candidateSha256,
571
+ expectedSubjectDigest: continuation.subjectDigest,
572
+ production: true,
573
+ offline: false,
574
+ offlineExplicit: false,
575
+ workflowExplicit: false,
576
+ version: state.targetVersion ?? undefined,
577
+ versionExplicit: state.targetVersion != null,
578
+ hooksAuthorized: true,
579
+ verificationGatesAuthorized: true,
580
+ hookCache: true,
581
+ ...(Array.isArray(state.selectedUnitIds) ? { unitIds: state.selectedUnitIds } : {}),
582
+ });
583
+ if (prepared?.status === 'NEEDS_INPUT') {
584
+ state = {
585
+ ...applyShipPrepareOutcome(state, {
586
+ ...prepared,
587
+ testCandidate: {
588
+ path: prepared.candidatePath ?? state.testCandidate.path,
589
+ sha256: prepared.candidateSha256 ?? state.testCandidate.sha256,
590
+ subjectDigest: prepared.subjectDigest ?? state.testCandidate.subjectDigest,
591
+ evidenceDir: prepared.evidenceDir ?? state.testCandidate.evidenceDir,
592
+ },
593
+ }),
594
+ updatedAt: new Date().toISOString(),
595
+ };
596
+ await writeJsonAtomic(statePath, state);
597
+ return publicState(state);
598
+ }
599
+ const outcome = applyShipPrepareOutcome(state, prepared);
600
+ // A reattached PARTIAL, PUBLISHED, or VERIFIED record stays that status.
601
+ // Returning here avoids a new approval and avoids falling through into
602
+ // reconcile, distribute, or verify in this same call.
603
+ if (outcome.status !== 'NEEDS_PLAN_APPROVAL') {
604
+ const continuation = outcome.status === 'VERIFIED'
605
+ ? await observedPostverifyContinuation(root, outcome.planPath)
606
+ : null;
607
+ state = {
608
+ ...outcome,
609
+ ...(continuation ?? {}),
610
+ updatedAt: new Date().toISOString(),
611
+ };
612
+ await writeJsonAtomic(statePath, state);
613
+ return publicState(state);
614
+ }
615
+ const maxApprovalMs = typeof deps.readMaxApprovalMs === 'function'
616
+ ? await deps.readMaxApprovalMs()
617
+ : MAX_APPROVAL_MS;
618
+ const approvalSummary = await buildApprovalSummary(prepared.planPath, maxApprovalMs);
619
+ state = {
620
+ ...outcome,
621
+ approvalSummary,
622
+ updatedAt: new Date().toISOString(),
623
+ };
624
+ await writeJsonAtomic(statePath, state);
625
+ } else if (state.status === 'NEW') {
487
626
  const loaded = await deps.loadProjectConfig({ root });
488
627
  let stateChanged = false;
489
628
  if (Array.isArray(state.selectedUnitIds)) {
@@ -538,6 +677,23 @@ export async function advanceShip(options = {}, injected = {}) {
538
677
  hookCache: true,
539
678
  ...(state.selectedUnitIds ? { unitIds: state.selectedUnitIds } : {}),
540
679
  });
680
+ if (prepared?.status === 'NEEDS_INPUT') {
681
+ state = {
682
+ ...applyShipPrepareOutcome(state, {
683
+ ...prepared,
684
+ testCandidate: {
685
+ path: prepared.candidatePath,
686
+ sha256: prepared.candidateSha256,
687
+ subjectDigest: prepared.subjectDigest,
688
+ evidenceDir: prepared.evidenceDir,
689
+ },
690
+ requirements: prepared.requirements,
691
+ }),
692
+ updatedAt: new Date().toISOString(),
693
+ };
694
+ await writeJsonAtomic(statePath, state);
695
+ return publicState(state);
696
+ }
541
697
  let frozenPlan = null;
542
698
  if (Array.isArray(state.selectedUnitIds) || deps.preflightGitTransports) {
543
699
  frozenPlan = Array.isArray(state.selectedUnitIds)
@@ -631,6 +787,9 @@ export async function advanceShip(options = {}, injected = {}) {
631
787
  }
632
788
 
633
789
  if (state.status === 'PARTIAL') {
790
+ // A reattached terminal record has no source run. Returning keeps that
791
+ // status; a publish that stored sourceRunPath still reconciles.
792
+ if (!state.sourceRunPath) return publicState(state);
634
793
  const reconciled = await deps.reconcileRelease({
635
794
  planPath: state.planPath,
636
795
  sourceRunPath: state.sourceRunPath,
@@ -653,6 +812,7 @@ export async function advanceShip(options = {}, injected = {}) {
653
812
  let postVerifyRanThisCall = false;
654
813
 
655
814
  if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
815
+ if (!state.sourceRunPath && !state.distributeRunPath) return publicState(state);
656
816
  // Step 1: Check if postPublish requires distribution.
657
817
  // Only targets and distribute-phase hooks route through distribute;
658
818
  // phase:postVerify hooks belong to their independent post-VERIFIED run.
@@ -699,10 +859,12 @@ export async function advanceShip(options = {}, injected = {}) {
699
859
  }
700
860
 
701
861
  // Step 2: Verify (this will check for distributed run if distribution was required)
862
+ const verifySource = needsDistribution ? state.distributeRunPath : state.sourceRunPath;
863
+ if (!verifySource) return publicState(state);
702
864
  try {
703
865
  const verified = await deps.verifyRelease({
704
866
  planPath: state.planPath,
705
- sourceRunPath: needsDistribution ? state.distributeRunPath : state.sourceRunPath,
867
+ sourceRunPath: verifySource,
706
868
  adapterRegistry: options.adapterRegistry,
707
869
  root,
708
870
  verificationGatesAuthorized: state.verificationGatesAuthorized === true,
@@ -737,7 +899,7 @@ export async function advanceShip(options = {}, injected = {}) {
737
899
  const postVerifyHooks = normalizePostPublishView(plan)
738
900
  .flatMap((declaration) => declaration.hooks ?? [])
739
901
  .filter((hook) => hook.phase === 'postVerify');
740
- if (state.status === 'VERIFIED' && postVerifyHooks.length > 0 && deps.postVerifyRelease) {
902
+ if (state.status === 'VERIFIED' && state.verifyRunPath && postVerifyHooks.length > 0 && deps.postVerifyRelease) {
741
903
  postVerifyRanThisCall = true;
742
904
  let postVerifyOutcome;
743
905
  try {
@@ -785,6 +947,7 @@ export async function advanceShip(options = {}, injected = {}) {
785
947
  if (
786
948
  state.status === 'VERIFIED'
787
949
  && deps.postVerifyRelease
950
+ && state.verifyRunPath
788
951
  && !postVerifyRanThisCall
789
952
  && (!state.postVerify || state.postVerify.status !== 'DISTRIBUTED')
790
953
  ) {
@@ -27,6 +27,7 @@ import { validatePublicSurfaceGlob } from './public-surface.mjs';
27
27
  import { canonicalPublicPath, publicPathCollisionKey } from '../snapshot/public-path.mjs';
28
28
  import { isReservedReleaseControlPath } from './baseline.mjs';
29
29
  import { readTrustedPackageResource } from './trusted-resource.mjs';
30
+ import { assertTestIntakePaths } from './test-intake.mjs';
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Constants
@@ -423,6 +424,10 @@ export async function loadProjectConfig({ root, configPath } = {}) {
423
424
  );
424
425
  }
425
426
 
427
+ if (config.testIntake) {
428
+ assertTestIntakePaths(config.testIntake);
429
+ }
430
+
426
431
  // --- Compile forbiddenContentPatterns (fail closed on invalid regex) ---
427
432
  const forbiddenContentPatterns = config.policy?.forbiddenContentPatterns ?? [];
428
433
  for (const pattern of forbiddenContentPatterns) {
package/src/core/plan.mjs CHANGED
@@ -210,7 +210,9 @@ export function validatePlan(plan) {
210
210
  * still written to the plan file (schema unchanged, evidence chain intact)
211
211
  * but is excluded from the digest. Everything else (planVersion, units with
212
212
  * every frozenSnapshot artifact identity field, externalActions identity,
213
- * production, configDigest, verificationGates, snapshotDigest) binds.
213
+ * production, configDigest, verificationGates, snapshotDigest, and
214
+ * testAcceptance when present) binds. testAcceptance is intentionally absent
215
+ * from the exclusion list below: it stays in the binding layer.
214
216
  * For v3 the frozen `postPublish` array and every item binding field stay in
215
217
  * the binding layer: any order or binding change moves the digest.
216
218
  *