@deksden-com/dd-flow-cli 0.9.0-beta.119 → 0.9.0-beta.121

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.
@@ -201,6 +201,7 @@ export function prepareVnextPlanArtifacts(context, input) {
201
201
  const batch = path.join(input.home, "03-plan", "code-work-batch.json");
202
202
  const ownership = protocolOwnership(input.home, input.protocols);
203
203
  const obligations = acceptedObligations(input.home);
204
+ const mergeEnding = runEndsAtMerge(context, input.projectRoot, input.runId);
204
205
  const frozenDocumentBaselines = input.publishBatch === false && fs.existsSync(batch)
205
206
  ? new Map(readJson(batch).works?.flatMap((work) => typeof work.key === "string" ? (work.document_updates ?? []).flatMap((update) => typeof update.path === "string" && (typeof update.baseline_sha256 === "string" || update.baseline_sha256 === null)
206
207
  ? [[`${work.key}\u0000${update.path}`, update.baseline_sha256]]
@@ -214,7 +215,7 @@ export function prepareVnextPlanArtifacts(context, input) {
214
215
  try {
215
216
  const { value } = prepareJsonSchema(file, "PLAN", { schemaName: "vnext-protocol-plan", projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
216
217
  assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
217
- validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations);
218
+ validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations, mergeEnding);
218
219
  validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
219
220
  for (const check of value.checks.filter((item) => item.availability === "available"))
220
221
  validateCheckDeclaration(workspaceRoot, check);
@@ -481,7 +482,7 @@ function normalizedAspectMapRefs(file, bytes, projectRoot, runHome, runId) {
481
482
  aspect.planned_artifact_refs = aspect.planned_artifact_refs.map(normalize);
482
483
  return changed ? `${JSON.stringify(map, null, 2)}\n` : bytes;
483
484
  }
484
- function projectCodeWorkBatch(input) {
485
+ export function projectCodeWorkBatch(input) {
485
486
  const obligationMap = acceptedObligationMap(input.home);
486
487
  const orientation = projectOrientation(input.workspaceRoot);
487
488
  const works = input.plans.flatMap(({ protocolId, file, value }) => value.items.map((item) => {
@@ -516,10 +517,9 @@ function projectCodeWorkBatch(input) {
516
517
  ])],
517
518
  discovery_boundary: item.execution_context.discovery_boundary,
518
519
  planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
519
- // A criterion belongs to every listed Work. Its proof must travel with
520
- // those Work packets as well as item-local verification; otherwise CODE
521
- // silently loses readiness/code checks declared only by acceptance.
522
- checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance).includes(check.id)),
520
+ // CODE still needs acceptance-only aggregate declarations in its packets;
521
+ // work checks belong only to the item that explicitly verifies them.
522
+ checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance, value.checks).includes(check.id)),
523
523
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
524
524
  stop_conditions: item.execution_context.stop_conditions,
525
525
  depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
@@ -548,10 +548,15 @@ function projectCodeWorkBatch(input) {
548
548
  const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
549
549
  return { schema_id: "dd-flow/code-work-batch@5", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
550
550
  }
551
- function itemCheckRefs(item, acceptance) {
552
- return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs)])];
551
+ function itemCheckRefs(item, acceptance, catalog) {
552
+ const aggregate = new Set(catalog.filter((check) => check.run_at !== "work").map((check) => check.id));
553
+ return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs.filter((ref) => aggregate.has(ref)))])];
554
+ }
555
+ function renderCodeTask(item, catalog, acceptance = []) {
556
+ const local = catalog.filter((check) => check.run_at === "work" && item.verification.check_refs.includes(check.id));
557
+ const aggregate = catalog.filter((check) => check.run_at !== "work" && itemCheckRefs(item, acceptance, catalog).includes(check.id));
558
+ return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Work checks: ${local.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Later aggregate checks (CODE/MERGE; do not run in this Work): ${aggregate.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n");
553
559
  }
554
- function renderCodeTask(item, catalog, acceptance = []) { const checks = catalog.filter((check) => itemCheckRefs(item, acceptance).includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
555
560
  function validateProjectedPaths(batch, workspaceRoot, home, runId) {
556
561
  for (const work of batch.works) {
557
562
  for (const write of work.planned_write_areas)
@@ -595,7 +600,7 @@ function resolvePortablePath(value, workspaceRoot, home, runId) {
595
600
  }
596
601
  function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
597
602
  function runRef(runId, runHome, file) { return `run://${runId}/${path.relative(runHome, file).split(path.sep).join("/")}`; }
598
- function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
603
+ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs, mergeEnding) {
599
604
  const items = plan.items ?? [];
600
605
  const ids = new Set(items.map((item) => item.id).filter((id) => Boolean(id)));
601
606
  if (ids.size !== items.length)
@@ -608,6 +613,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
608
613
  for (const check of plan.checks ?? []) {
609
614
  if (checks.has(check.id))
610
615
  throw new AppError("validation", "PLAN check ids must be unique", 2, { file, check_id: check.id });
616
+ if (mergeEnding && check.run_at === "release")
617
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot execute RELEASE checks", 2, { file, check_id: check.id });
611
618
  if (check.availability === "planned" && (!check.provided_by || !ids.has(check.provided_by)))
612
619
  throw new AppError("planned_check_provider_missing", "A planned check must name a PLAN item that provides it", 2, { file, check_id: check.id, provided_by: check.provided_by ?? null });
613
620
  if (check.availability === "planned" && !check.command.startsWith("@check/"))
@@ -616,6 +623,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
616
623
  throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
617
624
  checks.set(check.id, check);
618
625
  }
626
+ const localChecks = new Set(items.flatMap((item) => item.verification.check_refs));
627
+ const acceptanceIds = new Set();
619
628
  const ancestors = (itemId, seen = new Set()) => {
620
629
  for (const dependency of items.find((item) => item.id === itemId)?.depends_on ?? [])
621
630
  if (!seen.has(dependency)) {
@@ -639,7 +648,7 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
639
648
  const check = checks.get(checkId);
640
649
  if (!check)
641
650
  throw new AppError("check_reference_unknown", "PLAN item references an unknown check", 2, { file, item: item.id, check_id: checkId });
642
- if (check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
651
+ if (check.run_at === "work" && check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
643
652
  throw new AppError("check_consumer_not_ordered_after_provider", "A Work may consume a planned check only after its provider", 2, { file, item: item.id, check_id: checkId, provider: check.provided_by });
644
653
  }
645
654
  }
@@ -655,6 +664,11 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
655
664
  for (const acceptance of plan.acceptance ?? []) {
656
665
  if (!acceptance.criterion_id || !acceptance.criterion_id.startsWith("AC-") || !ownedRefs.has(acceptance.criterion_id))
657
666
  throw new AppError("validation", "PLAN acceptance references an AC-* owned by this protocol", 2, { file, criterion_id: acceptance.criterion_id });
667
+ if (acceptanceIds.has(acceptance.criterion_id))
668
+ throw new AppError("duplicate_plan_criterion", "PLAN acceptance criterion ids must be unique within a protocol", 2, { file, criterion_id: acceptance.criterion_id });
669
+ acceptanceIds.add(acceptance.criterion_id);
670
+ if (mergeEnding && acceptance.gate === "release")
671
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot prove RELEASE acceptance", 2, { file, criterion_id: acceptance.criterion_id });
658
672
  for (const id of acceptance.plan_item_ids ?? [])
659
673
  if (!ids.has(id))
660
674
  throw new AppError("validation", "PLAN acceptance references an unknown item", 2, { file, criterion_id: acceptance.criterion_id, plan_item_id: id });
@@ -662,9 +676,10 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
662
676
  const check = checks.get(id);
663
677
  if (!check)
664
678
  throw new AppError("check_reference_unknown", "PLAN acceptance references an unknown check", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
665
- for (const itemId of acceptance.plan_item_ids ?? [])
666
- if (check.availability === "planned" && check.provided_by !== itemId && !ancestors(itemId).has(check.provided_by))
667
- throw new AppError("check_consumer_not_ordered_after_provider", "Acceptance may consume a planned check only after its provider", 2, { file, criterion_id: acceptance.criterion_id, item: itemId, check_id: id, provider: check.provided_by });
679
+ if ((acceptance.gate !== "release" && check.run_at === "release") || (!["merge", "release"].includes(acceptance.gate) && check.run_at === "merge"))
680
+ throw new AppError("check_gate_unreachable", "PLAN acceptance references a check after its gate", 2, { file, criterion_id: acceptance.criterion_id, check_id: id, gate: acceptance.gate, run_at: check.run_at });
681
+ if (check.run_at === "work" && !localChecks.has(id))
682
+ throw new AppError("work_check_unassigned", "A work check referenced by acceptance needs an explicit verifying PLAN item", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
668
683
  }
669
684
  }
670
685
  for (const obligation of ownedRefs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.119",
3
+ "version": "0.9.0-beta.121",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {