perttool 0.11.0 → 0.12.0

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 (176) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +14 -8
  3. package/dist/application/cli-facade.d.ts +2 -1
  4. package/dist/application/cli-facade.d.ts.map +1 -1
  5. package/dist/application/cli-facade.js +2 -1
  6. package/dist/application/cli-facade.js.map +1 -1
  7. package/dist/application/contract11-candidate.d.ts +23 -0
  8. package/dist/application/contract11-candidate.d.ts.map +1 -0
  9. package/dist/application/contract11-candidate.js +48 -0
  10. package/dist/application/contract11-candidate.js.map +1 -0
  11. package/dist/application/contract11-migration.d.ts +35 -0
  12. package/dist/application/contract11-migration.d.ts.map +1 -0
  13. package/dist/application/contract11-migration.js +46 -0
  14. package/dist/application/contract11-migration.js.map +1 -0
  15. package/dist/application/contract11-runtime.d.ts +11 -0
  16. package/dist/application/contract11-runtime.d.ts.map +1 -0
  17. package/dist/application/contract11-runtime.js +65 -0
  18. package/dist/application/contract11-runtime.js.map +1 -0
  19. package/dist/application/contract8-milestone-acceptance.d.ts +6 -0
  20. package/dist/application/contract8-milestone-acceptance.d.ts.map +1 -1
  21. package/dist/application/contract8-milestone-acceptance.js +10 -1
  22. package/dist/application/contract8-milestone-acceptance.js.map +1 -1
  23. package/dist/application/contract9-projection.d.ts +1 -1
  24. package/dist/application/contract9-projection.d.ts.map +1 -1
  25. package/dist/application/contract9-projection.js.map +1 -1
  26. package/dist/application/plan-review-mutation.d.ts +52 -0
  27. package/dist/application/plan-review-mutation.d.ts.map +1 -0
  28. package/dist/application/plan-review-mutation.js +86 -0
  29. package/dist/application/plan-review-mutation.js.map +1 -0
  30. package/dist/assurance/canonical.d.ts +3 -1
  31. package/dist/assurance/canonical.d.ts.map +1 -1
  32. package/dist/assurance/canonical.js +2 -2
  33. package/dist/assurance/canonical.js.map +1 -1
  34. package/dist/cli.js +490 -132
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command/contract11-discovery.d.ts +17 -0
  37. package/dist/command/contract11-discovery.d.ts.map +1 -0
  38. package/dist/command/contract11-discovery.js +129 -0
  39. package/dist/command/contract11-discovery.js.map +1 -0
  40. package/dist/command/contract11-usage.d.ts +24 -0
  41. package/dist/command/contract11-usage.d.ts.map +1 -0
  42. package/dist/command/contract11-usage.js +39 -0
  43. package/dist/command/contract11-usage.js.map +1 -0
  44. package/dist/core/index.d.ts +2 -2
  45. package/dist/core/index.d.ts.map +1 -1
  46. package/dist/core/index.js +2 -1
  47. package/dist/core/index.js.map +1 -1
  48. package/dist/help/contract11-guide.d.ts +10 -0
  49. package/dist/help/contract11-guide.d.ts.map +1 -0
  50. package/dist/help/contract11-guide.js +73 -0
  51. package/dist/help/contract11-guide.js.map +1 -0
  52. package/dist/index.d.ts +19 -5
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +15 -4
  55. package/dist/index.js.map +1 -1
  56. package/dist/model/canonical-json.d.ts +3 -0
  57. package/dist/model/canonical-json.d.ts.map +1 -0
  58. package/dist/model/canonical-json.js +47 -0
  59. package/dist/model/canonical-json.js.map +1 -0
  60. package/dist/model/rfc8785.d.ts +2 -0
  61. package/dist/model/rfc8785.d.ts.map +1 -0
  62. package/dist/model/rfc8785.js +47 -0
  63. package/dist/model/rfc8785.js.map +1 -0
  64. package/dist/plan-review/advance.d.ts +28 -0
  65. package/dist/plan-review/advance.d.ts.map +1 -0
  66. package/dist/plan-review/advance.js +67 -0
  67. package/dist/plan-review/advance.js.map +1 -0
  68. package/dist/plan-review/authority.d.ts +4 -0
  69. package/dist/plan-review/authority.d.ts.map +1 -0
  70. package/dist/plan-review/authority.js +21 -0
  71. package/dist/plan-review/authority.js.map +1 -0
  72. package/dist/plan-review/basis.d.ts +25 -0
  73. package/dist/plan-review/basis.d.ts.map +1 -0
  74. package/dist/plan-review/basis.js +287 -0
  75. package/dist/plan-review/basis.js.map +1 -0
  76. package/dist/plan-review/format.d.ts +5 -0
  77. package/dist/plan-review/format.d.ts.map +1 -0
  78. package/dist/plan-review/format.js +54 -0
  79. package/dist/plan-review/format.js.map +1 -0
  80. package/dist/plan-review/lifecycle.d.ts +11 -0
  81. package/dist/plan-review/lifecycle.d.ts.map +1 -0
  82. package/dist/plan-review/lifecycle.js +80 -0
  83. package/dist/plan-review/lifecycle.js.map +1 -0
  84. package/dist/plan-review/mutation-types.d.ts +112 -0
  85. package/dist/plan-review/mutation-types.d.ts.map +1 -0
  86. package/dist/plan-review/mutation-types.js +4 -0
  87. package/dist/plan-review/mutation-types.js.map +1 -0
  88. package/dist/plan-review/mutation.d.ts +4 -0
  89. package/dist/plan-review/mutation.d.ts.map +1 -0
  90. package/dist/plan-review/mutation.js +421 -0
  91. package/dist/plan-review/mutation.js.map +1 -0
  92. package/dist/plan-review/projection.d.ts +4 -0
  93. package/dist/plan-review/projection.d.ts.map +1 -0
  94. package/dist/plan-review/projection.js +19 -0
  95. package/dist/plan-review/projection.js.map +1 -0
  96. package/dist/plan-review/request-size.d.ts +3 -0
  97. package/dist/plan-review/request-size.d.ts.map +1 -0
  98. package/dist/plan-review/request-size.js +86 -0
  99. package/dist/plan-review/request-size.js.map +1 -0
  100. package/dist/plan-review/request.d.ts +11 -0
  101. package/dist/plan-review/request.d.ts.map +1 -0
  102. package/dist/plan-review/request.js +268 -0
  103. package/dist/plan-review/request.js.map +1 -0
  104. package/dist/plan-review/source-lexical.d.ts +23 -0
  105. package/dist/plan-review/source-lexical.d.ts.map +1 -0
  106. package/dist/plan-review/source-lexical.js +75 -0
  107. package/dist/plan-review/source-lexical.js.map +1 -0
  108. package/dist/plan-review/source-types.d.ts +81 -0
  109. package/dist/plan-review/source-types.d.ts.map +1 -0
  110. package/dist/plan-review/source-types.js +2 -0
  111. package/dist/plan-review/source-types.js.map +1 -0
  112. package/dist/plan-review/source.d.ts +16 -0
  113. package/dist/plan-review/source.d.ts.map +1 -0
  114. package/dist/plan-review/source.js +346 -0
  115. package/dist/plan-review/source.js.map +1 -0
  116. package/dist/plan-review/write.d.ts +4 -0
  117. package/dist/plan-review/write.d.ts.map +1 -0
  118. package/dist/plan-review/write.js +51 -0
  119. package/dist/plan-review/write.js.map +1 -0
  120. package/dist/planning-pool/history-guard.d.ts +7 -1
  121. package/dist/planning-pool/history-guard.d.ts.map +1 -1
  122. package/dist/planning-pool/history-guard.js +13 -12
  123. package/dist/planning-pool/history-guard.js.map +1 -1
  124. package/dist/planning-pool/projection.d.ts +1 -1
  125. package/dist/planning-pool/projection.d.ts.map +1 -1
  126. package/dist/planning-pool/projection.js.map +1 -1
  127. package/dist/planning-pool/source-lexical.d.ts.map +1 -1
  128. package/dist/planning-pool/source-lexical.js +2 -22
  129. package/dist/planning-pool/source-lexical.js.map +1 -1
  130. package/dist/recommendation/override-types.d.ts +2 -2
  131. package/dist/recommendation/override-types.d.ts.map +1 -1
  132. package/dist/recommendation/override.d.ts +1 -1
  133. package/dist/recommendation/override.d.ts.map +1 -1
  134. package/dist/recommendation/override.js +6 -5
  135. package/dist/recommendation/override.js.map +1 -1
  136. package/dist/schema/registry.d.ts +1 -1
  137. package/dist/schema/registry.d.ts.map +1 -1
  138. package/dist/schema/registry.js +5 -2
  139. package/dist/schema/registry.js.map +1 -1
  140. package/dist/temporal-schedule/source-lexical.d.ts +5 -0
  141. package/dist/temporal-schedule/source-lexical.d.ts.map +1 -1
  142. package/dist/temporal-schedule/source-lexical.js +21 -4
  143. package/dist/temporal-schedule/source-lexical.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.d.ts.map +1 -1
  146. package/dist/version.js +2 -1
  147. package/dist/version.js.map +1 -1
  148. package/package.json +1 -1
  149. package/schemas/Perttool.AdvanceResult.v4.schema.json +4 -1
  150. package/schemas/Perttool.AgentGuidanceResult.v1.schema.json +1 -1
  151. package/schemas/Perttool.AnalysisResult.v7.schema.json +1 -1
  152. package/schemas/Perttool.CheckResult.v6.schema.json +1 -1
  153. package/schemas/Perttool.CliError.v1.schema.json +1 -1
  154. package/schemas/Perttool.CommandHelpResult.v1.schema.json +2 -2
  155. package/schemas/Perttool.ExportResult.v1.schema.json +1 -1
  156. package/schemas/Perttool.FormatResult.v1.schema.json +1 -1
  157. package/schemas/Perttool.GuideResult.v1.schema.json +1 -1
  158. package/schemas/Perttool.HistoricalGraphResult.v1.schema.json +1 -1
  159. package/schemas/Perttool.ImportResult.v1.schema.json +1 -1
  160. package/schemas/Perttool.InitResult.v1.schema.json +1 -1
  161. package/schemas/Perttool.MilestoneAcceptanceMigrationResult.v1.schema.json +1 -1
  162. package/schemas/Perttool.MilestoneAcceptanceResult.v1.schema.json +1 -1
  163. package/schemas/Perttool.MutationResult.v6.schema.json +1 -1
  164. package/schemas/{Perttool.NextResult.v8.schema.json → Perttool.NextResult.v9.schema.json} +40 -7
  165. package/schemas/Perttool.PlanAssuranceResult.v2.schema.json +1 -1
  166. package/schemas/Perttool.PlanReviewMutationResult.v1.schema.json +2203 -0
  167. package/schemas/Perttool.PlanReviewResult.v1.schema.json +245 -0
  168. package/schemas/Perttool.PlanningMutationResult.v1.schema.json +1 -1
  169. package/schemas/Perttool.PlanningPoolResult.v1.schema.json +1 -1
  170. package/schemas/Perttool.PlanningReshapePreflightResult.v1.schema.json +1 -1
  171. package/schemas/Perttool.ProjectHistoryResult.v1.schema.json +1 -1
  172. package/schemas/Perttool.ProjectResult.v5.schema.json +1 -1
  173. package/schemas/Perttool.SchemaResult.v1.schema.json +1 -1
  174. package/schemas/Perttool.UnitMigrationResult.v5.schema.json +1 -1
  175. package/schemas/Perttool.VelocityObservationResult.v1.schema.json +1 -1
  176. package/dist/perttool-vscode-private-0.0.0-d1578b9.vsix +0 -0
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // R: Execute the active CLI contract and bind its results to document and persistence ports.
3
+ import { createReadStream } from "node:fs";
2
4
  import { lstat } from "node:fs/promises";
3
5
  import process from "node:process";
4
6
  import { TextDecoder } from "node:util";
@@ -7,11 +9,11 @@ import { createNodeHost } from "./node/host.js";
7
9
  import { createHistoricalGraphGitEvidenceHost } from "./node/historical-host.js";
8
10
  import { exportMermaid, } from "./conversion/mermaid.js";
9
11
  import { importMermaid } from "./conversion/mermaid-import.js";
10
- import { getContract10Guide as getAssuranceGuide, renderContract10GuideResult as renderAssuranceGuideResult, serializeContract10GuideResult as serializeAssuranceGuideResult, } from "./help/contract10-guide.js";
12
+ import { getContract11Guide as getAssuranceGuide, renderContract11GuideResult as renderAssuranceGuideResult, serializeContract11GuideResult as serializeAssuranceGuideResult, } from "./help/contract11-guide.js";
11
13
  import { commandOptionSets, } from "./command/registry.js";
12
- import { CONTRACT10_COMMAND_REGISTRY as ASSURANCE_COMMAND_REGISTRY, getContract10CommandDiscovery as getAssuranceCommandDiscovery, renderContract10CommandHelpResult as renderAssuranceCommandHelpResult, serializeContract10CommandHelpResult as serializeAssuranceCommandHelpResult, } from "./command/contract10-discovery.js";
14
+ import { CONTRACT11_COMMAND_REGISTRY as ASSURANCE_COMMAND_REGISTRY, getContract11CommandDiscovery as getAssuranceCommandDiscovery, renderContract11CommandHelpResult as renderAssuranceCommandHelpResult, serializeContract11CommandHelpResult as serializeAssuranceCommandHelpResult, } from "./command/contract11-discovery.js";
13
15
  import { handlerCommandUsageError, } from "./command/usage.js";
14
- import { renderContract10CommandUsageError as renderAssuranceCommandUsageError, serializeContract10CommandUsageError as serializeAssuranceCommandUsageError, validateContract10CommandInvocation as validateAssuranceCommandInvocation, } from "./command/contract10-usage.js";
16
+ import { renderContract11CommandUsageError as renderAssuranceCommandUsageError, serializeContract11CommandUsageError as serializeAssuranceCommandUsageError, validateContract11CommandInvocation as validateAssuranceCommandInvocation, } from "./command/contract11-usage.js";
15
17
  import { agentGuidanceResultToJson } from "./guidance/projection.js";
16
18
  import { agentGuidanceExitCode, renderAgentGuidanceText, } from "./guidance/text.js";
17
19
  import { SafeWriteConflictError, SafeWriteVerificationError, } from "./io/safe-write.js";
@@ -27,9 +29,21 @@ import { TARGET_GRAMMAR_6_CAPABILITY, } from "./parser/document-parser.js";
27
29
  import { exportPlanAssuranceMermaid, importPlanAssuranceMermaid, } from "./assurance/mermaid.js";
28
30
  import { getJsonSchemaResult, renderJsonSchemaResult, serializeJsonSchemaResult, } from "./schema/registry.js";
29
31
  import { TOOL_VERSION } from "./version.js";
30
- import { analyzeDocument as analyzeContract8Document, checkDocument as checkContract8Document, planGrammarMigration as planContract10GrammarMigration, selectNextTasks as selectContract8NextTasks, } from "./application/contract10-runtime.js";
32
+ import { checkDocument as checkContract8Document, planGrammarMigration as planContract10GrammarMigration, } from "./application/contract10-runtime.js";
33
+ import { analyzeDocument as analyzeContract11Document, checkDocument as checkContract11Document, selectNextTasks as selectContract11NextTasks, planReviewProjection, } from "./application/contract11-runtime.js";
34
+ import { planPlanReviewCreate, planPlanReviewResolve } from "./application/plan-review-mutation.js";
35
+ import { parsePlanReviewSource, PLAN_REVIEW_SOURCE_CAPABILITY } from "./plan-review/source.js";
36
+ import { projectPlanReviewState } from "./plan-review/projection.js";
37
+ import { formatPlanReviewSource } from "./plan-review/format.js";
38
+ import { persistPlanReviewMutation } from "./plan-review/write.js";
39
+ import { PLAN_REVIEW_CREATE_REQUEST_ID, PLAN_REVIEW_RESOLVE_REQUEST_ID } from "./plan-review/mutation-types.js";
40
+ import { normalizePlanReviewCreateRequest, normalizePlanReviewResolveRequest } from "./plan-review/request.js";
41
+ import { planReviewBaseText, scanPlanReviewDeclarationBlocks } from "./plan-review/source-lexical.js";
42
+ import { composePlanReviewAdvanceCandidate, assessPlanReviewAdvanceHistory } from "./plan-review/advance.js";
31
43
  import { contract9CheckResultToJson, contract9ProjectResultToJson, liftContract9AnalysisResultJson, liftContract9NextResultJson, renderContract9ScheduleAlerts, contract9WireJson, } from "./application/contract9-projection.js";
32
44
  import { planContract9GrammarMigration } from "./application/contract9-format-migration.js";
45
+ import { planContract11GrammarMigration } from "./application/contract11-migration.js";
46
+ import { liftContract11Candidate } from "./application/contract11-candidate.js";
33
47
  import { planContract9TemporalMutation } from "./application/contract9-temporal-mutation.js";
34
48
  import { composeContract9MixedMutation, composeContract9TemporalMutation } from "./application/contract9-mixed-mutation.js";
35
49
  import { planAcceptanceReceiptMutation, planCriterionSetReplacement, showMilestoneAcceptance, } from "./milestone-acceptance/mutation.js";
@@ -48,9 +62,9 @@ import { compilePlanningIntentRequest, isPlanningIntentRequest, } from "./planni
48
62
  import { renderPlanningHistoricalObservationText, renderPlanningObservationText, renderPlanningPoolReadText, renderPlanningReshapePreflightText, } from "./planning-pool/human.js";
49
63
  const historicalGitEvidenceHost = createHistoricalGraphGitEvidenceHost();
50
64
  const { analyzeDocument: _analyzeContract7Document, checkDocument: _checkContract7Document, selectNextTasks: _selectContract7NextTasks, planFormat, planProjectInit, projectInitResultToJson, renderProjectInitResult, withProjectInitOutput, planUnitMigration, withUnitMigrationWrite, getProjectMetadata, contract7ProjectResultToJson, renderContract7ProjectText, planAdvance, planBatchMutation, planFinishActuals, planLifecycle, planMutation, planAssuranceMutation, renderTargetGovernanceDecision, contract7InspectionResultToJson, contract7MutationResultToJson, contract7SnakeJson, contract6WorkEventToJson, renderAdvanceHistoryGuard, prepareTargetPlanAssuranceAdvanceHistory, prepareAdvanceHistory, withAdvanceHistoryRace, withTargetPlanAssuranceAdvanceHistoryRace, persistTargetPlanAssuranceResult, inspectTargetPlanAssurance, inspectTargetCurrentProjectActuals, inspectTargetProjectHistoryFile, renderTargetProjectHistoryText, targetProjectHistoryResultToJson, observeTargetProjectVelocity, renderTargetVelocityObservationText, targetVelocityObservationResultToJson, getAgentHelp, documentContentFromBytes, readDocumentContent, readBytes, recheckAdvanceHistoryBaseline, captureAdvanceHistoryBaseline, createArtifactFile, createValidatedDocumentFile, createTargetGrammar6DocumentFile, replaceTargetGrammar6DocumentFile, replaceValidatedDocumentFile, inspectTargetHistoricalGraphFile, renderTargetHistoricalGraphText, targetHistoricalGraphResultToJson, } = createCliApplicationFacade(createNodeHost(), historicalGitEvidenceHost);
51
- const analyzeDocument = analyzeContract8Document;
52
- const checkDocument = checkContract8Document;
53
- const selectNextTasks = selectContract8NextTasks;
65
+ const analyzeDocument = analyzeContract11Document;
66
+ const checkDocument = checkContract11Document;
67
+ const selectNextTasks = selectContract11NextTasks;
54
68
  function contract9MutationResultToJson(result, ...args) {
55
69
  return Object.freeze({
56
70
  ...contract7MutationResultToJson(result, ...args),
@@ -231,7 +245,7 @@ async function readStdin() {
231
245
  function writeJson(value) {
232
246
  const contract8Value = typeof value === "object" && value !== null && !Array.isArray(value) &&
233
247
  "cli_contract_version" in value
234
- ? { ...value, cli_contract_version: 10 }
248
+ ? { ...value, cli_contract_version: 11 }
235
249
  : value;
236
250
  process.stdout.write(`${JSON.stringify(contract8Value)}\n`);
237
251
  }
@@ -542,20 +556,25 @@ async function persistContract8CandidateResult(result, request) {
542
556
  if (!result.ok || !result.governance?.writeAuthorized || result.updatedText === null) {
543
557
  throw new TypeError("authorized Contract 8 result does not contain a digest-bound candidate");
544
558
  }
545
- const validator = /^ version 9$/mu.test(result.updatedText)
559
+ const validator = /^ version 10$/mu.test(result.updatedText)
546
560
  ? (candidate) => {
547
- const parsed = parsePlanningPoolSource(candidate, PLANNING_POOL_SOURCE_CAPABILITY);
561
+ const parsed = parsePlanReviewSource(candidate, PLAN_REVIEW_SOURCE_CAPABILITY);
548
562
  return { ok: parsed.ok, diagnostics: parsed.diagnostics };
549
563
  }
550
- : /^ version 8$/mu.test(result.updatedText)
564
+ : /^ version 9$/mu.test(result.updatedText)
551
565
  ? (candidate) => {
552
- const checked = checkDocument(candidate);
553
- return { ok: checked.ok, diagnostics: checked.diagnostics };
566
+ const parsed = parsePlanningPoolSource(candidate, PLANNING_POOL_SOURCE_CAPABILITY);
567
+ return { ok: parsed.ok, diagnostics: parsed.diagnostics };
554
568
  }
555
- : (candidate) => {
556
- const parsed = parseMilestoneAcceptanceSource(candidate, MILESTONE_ACCEPTANCE_SOURCE_CAPABILITY);
557
- return { ok: parsed.ok, diagnostics: [] };
558
- };
569
+ : /^ version 8$/mu.test(result.updatedText)
570
+ ? (candidate) => {
571
+ const checked = checkDocument(candidate);
572
+ return { ok: checked.ok, diagnostics: checked.diagnostics };
573
+ }
574
+ : (candidate) => {
575
+ const parsed = parseMilestoneAcceptanceSource(candidate, MILESTONE_ACCEPTANCE_SOURCE_CAPABILITY);
576
+ return { ok: parsed.ok, diagnostics: [] };
577
+ };
559
578
  if (request.mode === "in_place") {
560
579
  await replaceValidatedDocumentFile(request.target, result.updatedText, {
561
580
  initialDigest: result.originalDigest,
@@ -578,6 +597,13 @@ async function commitCandidate(request, candidateText, initialDigest) {
578
597
  if (candidateText === null) {
579
598
  throw new SafeWriteVerificationError("invalid_candidate", "successful editing result has no candidate text");
580
599
  }
600
+ if (/^ version 10$/mu.test(candidateText)) {
601
+ const validator = (candidate) => {
602
+ const parsed = parsePlanReviewSource(candidate, PLAN_REVIEW_SOURCE_CAPABILITY);
603
+ return { ok: parsed.ok, diagnostics: parsed.diagnostics };
604
+ };
605
+ return commitValidatedCandidate(request, candidateText, initialDigest, validator);
606
+ }
581
607
  if (/^ version 9$/mu.test(candidateText)) {
582
608
  const validator = (candidate) => {
583
609
  const parsed = parsePlanningPoolSource(candidate, PLANNING_POOL_SOURCE_CAPABILITY);
@@ -668,37 +694,15 @@ function emitGrammarMigrationResult(format, result, sourceOperand, sourceDigest,
668
694
  process.stdout.write(diffOnly ? result.diff ?? "" : result.updatedText);
669
695
  }
670
696
  }
671
- async function runContract9DocumentMigration(args) {
672
- const parsed = parseCommandOptions("document.migrate", args);
673
- if (parsed.positionals.length !== 1 || parsed.values.get("target-grammar") !== "8")
674
- throw new UsageError("document migrate requires <file> --target-grammar 8");
675
- const sourceOperand = parsed.positionals[0];
676
- const format = outputFormat(parsed.values.get("format"));
677
- const input = await readDocument(sourceOperand);
678
- const result = planContract9GrammarMigration(input.text);
679
- let written = false;
680
- if (result.ok && result.updatedText !== null && parsed.flags.has("write")) {
681
- if (sourceOperand === "-")
682
- throw new UsageError("document migrate --write requires a file");
683
- await replaceValidatedDocumentFile(sourceOperand, result.updatedText, { initialDigest: input.digest, ...(parsed.values.get("expect-digest") === undefined ? {} : { expectedDigest: parsed.values.get("expect-digest") }) }, (candidate) => ({ ok: checkDocument(candidate).ok, diagnostics: [] }));
684
- written = result.changed;
685
- }
686
- emitGrammarMigrationResult(format, result, sourceOperand, input.digest, Object.freeze({
687
- mode: parsed.flags.has("write") ? "in_place" : "preview",
688
- target: parsed.flags.has("write") ? sourceOperand : null,
689
- written,
690
- }), parsed.flags.has("diff"));
691
- return result.ok ? 0 : 1;
692
- }
693
- async function runContract10DocumentMigration(args) {
697
+ async function runVersionMigration(args, targetGrammar, planner, validate) {
694
698
  const parsed = parseCommandOptions("document.migrate", args);
695
- if (parsed.positionals.length !== 1 || parsed.values.get("target-grammar") !== "9") {
696
- throw new UsageError("document migrate requires <file> --target-grammar 9");
699
+ if (parsed.positionals.length !== 1 || parsed.values.get("target-grammar") !== targetGrammar) {
700
+ throw new UsageError(`document migrate requires <file> --target-grammar ${targetGrammar}`);
697
701
  }
698
702
  const sourceOperand = parsed.positionals[0];
699
703
  const format = outputFormat(parsed.values.get("format"));
700
704
  const input = await readDocument(sourceOperand);
701
- const result = planContract10GrammarMigration(input.text);
705
+ const result = planner(input.text);
702
706
  let written = false;
703
707
  if (result.ok && result.updatedText !== null && parsed.flags.has("write")) {
704
708
  if (sourceOperand === "-")
@@ -708,10 +712,7 @@ async function runContract10DocumentMigration(args) {
708
712
  ...(parsed.values.get("expect-digest") === undefined
709
713
  ? {}
710
714
  : { expectedDigest: parsed.values.get("expect-digest") }),
711
- }, (candidate) => {
712
- const checked = parsePlanningPoolSource(candidate, PLANNING_POOL_SOURCE_CAPABILITY);
713
- return { ok: checked.ok, diagnostics: checked.diagnostics };
714
- });
715
+ }, validate);
715
716
  written = result.changed;
716
717
  }
717
718
  emitGrammarMigrationResult(format, result, sourceOperand, input.digest, Object.freeze({
@@ -721,6 +722,24 @@ async function runContract10DocumentMigration(args) {
721
722
  }), parsed.flags.has("diff"));
722
723
  return result.ok ? 0 : 1;
723
724
  }
725
+ async function runSupportedGrammarMigration(args) {
726
+ const target = args.includes("--target-grammar")
727
+ ? args[args.indexOf("--target-grammar") + 1]
728
+ : args.find((value) => value.startsWith("--target-grammar="))?.slice("--target-grammar=".length);
729
+ if (target === "8")
730
+ return runVersionMigration(args, "8", planContract9GrammarMigration, (candidate) => ({ ok: checkDocument(candidate).ok, diagnostics: [] }));
731
+ if (target === "9")
732
+ return runVersionMigration(args, "9", planContract10GrammarMigration, (candidate) => {
733
+ const checked = parsePlanningPoolSource(candidate, PLANNING_POOL_SOURCE_CAPABILITY);
734
+ return { ok: checked.ok, diagnostics: checked.diagnostics };
735
+ });
736
+ if (target === "10")
737
+ return runVersionMigration(args, "10", planContract11GrammarMigration, (candidate) => {
738
+ const checked = parsePlanReviewSource(candidate, PLAN_REVIEW_SOURCE_CAPABILITY);
739
+ return { ok: checked.ok, diagnostics: checked.diagnostics };
740
+ });
741
+ return null;
742
+ }
724
743
  async function runFormat(args) {
725
744
  const parsed = parseCommandOptions("document.format", args);
726
745
  if (parsed.positionals.length !== 1) {
@@ -739,11 +758,23 @@ async function runFormat(args) {
739
758
  catch (error) {
740
759
  return cliError(error instanceof Error ? error : new Error(String(error)), 3, "document.format", format === "json");
741
760
  }
742
- const result = planFormat(input.text, {
743
- maxDiagnostics,
744
- originalLabel: source,
745
- updatedLabel: "candidate",
746
- });
761
+ const result = /^ version 10$/mu.test(input.text)
762
+ ? (() => {
763
+ const formatted = formatPlanReviewSource(input.text, PLAN_REVIEW_SOURCE_CAPABILITY, { maxDiagnostics });
764
+ return Object.freeze({
765
+ ...formatted, originalDigest: input.digest,
766
+ updatedDigest: formatted.formattedText === null ? null
767
+ : sha256DigestUtf8(formatted.formattedText),
768
+ updatedText: formatted.formattedText,
769
+ diff: formatted.formattedText === null ? null
770
+ : createUnifiedDiff(input.text, formatted.formattedText, { originalLabel: source, updatedLabel: "candidate" }),
771
+ });
772
+ })()
773
+ : planFormat(input.text, {
774
+ maxDiagnostics,
775
+ originalLabel: source,
776
+ updatedLabel: "candidate",
777
+ });
747
778
  const warningFailure = parsed.flags.has("warnings-as-errors") &&
748
779
  (result.diagnosticsTruncated ||
749
780
  result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
@@ -1258,6 +1289,30 @@ async function readMutationRequest(source) {
1258
1289
  throw new UsageError(`mutation request is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1259
1290
  }
1260
1291
  }
1292
+ class PlanReviewBatchTooLarge extends Error {
1293
+ }
1294
+ async function readPlanReviewBatchRequest(source) {
1295
+ const maximumBytes = 8_388_608;
1296
+ const chunks = [];
1297
+ let totalBytes = 0;
1298
+ const stream = source === "-" ? process.stdin : createReadStream(source, {
1299
+ highWaterMark: 64 * 1024,
1300
+ });
1301
+ for await (const chunk of stream) {
1302
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1303
+ totalBytes += bytes.length;
1304
+ if (totalBytes > maximumBytes)
1305
+ throw new PlanReviewBatchTooLarge();
1306
+ chunks.push(bytes);
1307
+ }
1308
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
1309
+ try {
1310
+ return JSON.parse(text);
1311
+ }
1312
+ catch (error) {
1313
+ throw new UsageError(`mutation request is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1314
+ }
1315
+ }
1261
1316
  async function runMutation(resource, action, args) {
1262
1317
  const parsed = parseCommandOptions(`${resource}.${action}`, args);
1263
1318
  const format = outputFormat(parsed.values.get("format"));
@@ -1357,7 +1412,7 @@ async function runMutation(resource, action, args) {
1357
1412
  const temporalRequest = contract9Invocation.ok
1358
1413
  ? planContract9TemporalMutation(input.text, contract9Invocation, { maxDiagnostics })
1359
1414
  : null;
1360
- const legacyPreview = legacyPlanner(input.text);
1415
+ const legacyPreview = liftContract11Candidate(input.text, legacyPlanner, { originalLabel: source, updatedLabel: "candidate" });
1361
1416
  const result = temporalRequest === null
1362
1417
  ? legacyPreview
1363
1418
  : !legacyPreview.ok
@@ -1442,12 +1497,12 @@ async function runLifecycleMutation(action, args) {
1442
1497
  catch (error) {
1443
1498
  return cliError(error instanceof Error ? error : new Error(String(error)), 3, operation, format === "json");
1444
1499
  }
1445
- const result = planLifecycle(input.text, mutation, {
1500
+ const result = liftContract11Candidate(input.text, (candidate) => planLifecycle(candidate, mutation, {
1446
1501
  maxDiagnostics,
1447
1502
  originalLabel: source,
1448
1503
  updatedLabel: "candidate",
1449
1504
  governance: governanceRequest(parsed, writeRequest),
1450
- });
1505
+ }), { originalLabel: source, updatedLabel: "candidate" });
1451
1506
  const warningFailure = parsed.flags.has("warnings-as-errors") &&
1452
1507
  (result.diagnosticsTruncated ||
1453
1508
  result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
@@ -1800,6 +1855,9 @@ function renderAdvanceSummary(details) {
1800
1855
  return [
1801
1856
  `ADVANCE removed_tasks=${list(details.removedTaskIds)} removed_gates=${list(details.removedGateIds)} removed_milestones=${list(details.removedMilestoneIds)} removed_work_events=${list("removedWorkEventIds" in details ? details.removedWorkEventIds : [])}`,
1802
1857
  `ADVANCE removed_planning_links=${list("removedPlanningLinks" in details ? details.removedPlanningLinks : [])} archived_works=${list("archivedWorkIds" in details ? details.archivedWorkIds : [])}`,
1858
+ ...("removedPlanReviewRequestIds" in details
1859
+ ? [`ADVANCE removed_plan_review_requests=${list(details.removedPlanReviewRequestIds)}`]
1860
+ : []),
1803
1861
  `ADVANCE frontier_before=${list(details.frontierBefore)} frontier_after=${list(details.frontierAfter)} ready_before=${list(details.readyBefore)} ready_after=${list(details.readyAfter)}`,
1804
1862
  "",
1805
1863
  ].join("\n");
@@ -1829,6 +1887,22 @@ function renderAssuranceGuard(guard) {
1829
1887
  "",
1830
1888
  ].join("\n");
1831
1889
  }
1890
+ function mergeOuterAdvanceHistoryGuards(base, guards) {
1891
+ return guards.reduce((current, guard) => Object.freeze({
1892
+ ...current,
1893
+ status: guard.status === "blocked" ? "blocked"
1894
+ : guard.status === "forced" && current.status !== "blocked" ? "forced"
1895
+ : current.status,
1896
+ cause: guard.status === "passed" || guard.status === "not_applicable"
1897
+ ? current.cause : guard.cause,
1898
+ destructiveEntityIds: Object.freeze([...new Set([
1899
+ ...current.destructiveEntityIds, ...guard.destructiveEntityIds,
1900
+ ])].sort()),
1901
+ overlappingEntityIds: Object.freeze([...new Set([
1902
+ ...current.overlappingEntityIds, ...guard.overlappingEntityIds,
1903
+ ])].sort()),
1904
+ }), base);
1905
+ }
1832
1906
  async function runAdvance(args) {
1833
1907
  const parsed = parseCommandOptions("dag.advance", args);
1834
1908
  if (parsed.positionals.length !== 1) {
@@ -1851,10 +1925,28 @@ async function runAdvance(args) {
1851
1925
  catch (error) {
1852
1926
  return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.advance", format === "json");
1853
1927
  }
1854
- const grammar9 = /^ version 9$/mu.test(input.text);
1855
- const strictInputText = grammar9
1856
- ? planningPoolBaseText(input.text, scanPlanningDeclarationBlocks(input.text))
1928
+ const grammar10 = /^ version 10$/mu.test(input.text);
1929
+ const reviewSource = grammar10
1930
+ ? parsePlanReviewSource(input.text, PLAN_REVIEW_SOURCE_CAPABILITY, { maxDiagnostics })
1931
+ : null;
1932
+ if (reviewSource !== null && !reviewSource.ok) {
1933
+ if (format === "json")
1934
+ writeJson({ schema_version: "Perttool.CliError.v1",
1935
+ cli_contract_version: 11, tool_version: TOOL_VERSION, operation: "dag.advance",
1936
+ ok: false, diagnostics: reviewSource.diagnostics.map((value) => jsonDiagnostic(value)) });
1937
+ else
1938
+ for (const value of reviewSource.diagnostics) {
1939
+ process.stderr.write(`${renderDiagnostic(value, source, color)}\n`);
1940
+ }
1941
+ return 1;
1942
+ }
1943
+ const reviewBaseText = grammar10
1944
+ ? planReviewBaseText(input.text, scanPlanReviewDeclarationBlocks(input.text))
1857
1945
  : input.text;
1946
+ const grammar9 = /^ version 9$/mu.test(reviewBaseText);
1947
+ const strictInputText = grammar9
1948
+ ? planningPoolBaseText(reviewBaseText, scanPlanningDeclarationBlocks(reviewBaseText))
1949
+ : reviewBaseText;
1858
1950
  const acceptanceInputText = /^ version 8$/mu.test(strictInputText)
1859
1951
  ? temporalScheduleBaseText(strictInputText, scanTemporalDeclarationBlocks(strictInputText))
1860
1952
  : strictInputText;
@@ -1872,7 +1964,7 @@ async function runAdvance(args) {
1872
1964
  if (format === "json")
1873
1965
  writeJson({
1874
1966
  schema_version: "Perttool.AdvanceResult.v4",
1875
- cli_contract_version: 10,
1967
+ cli_contract_version: 11,
1876
1968
  tool_version: TOOL_VERSION,
1877
1969
  operation: "dag.advance",
1878
1970
  ok: false,
@@ -1924,22 +2016,37 @@ async function runAdvance(args) {
1924
2016
  ...acceptancePlan.provisional.edits,
1925
2017
  ]), "Contract 8 acceptance-aware advance");
1926
2018
  const planningCleanup = grammar9 && contract7Planned.updatedText !== null
1927
- ? planPlanningAdvanceCleanup(input.text, contract7Planned.advance?.removedMilestoneIds ?? [], contract7Planned.advance?.removedTaskIds ?? [], parsed.flags.has("archive-empty-work"))
2019
+ ? planPlanningAdvanceCleanup(reviewBaseText, contract7Planned.advance?.removedMilestoneIds ?? [], contract7Planned.advance?.removedTaskIds ?? [], parsed.flags.has("archive-empty-work"))
1928
2020
  : Object.freeze({
1929
2021
  edits: Object.freeze([]),
1930
2022
  removedPlanningLinks: Object.freeze([]),
1931
2023
  archivedWorkIds: Object.freeze([]),
1932
2024
  destructiveRecords: Object.freeze([]),
1933
2025
  });
1934
- const combinedEdits = contract7Planned.updatedText === null
2026
+ const lowerCombinedEdits = contract7Planned.updatedText === null
1935
2027
  ? strictCombinedEdits
1936
- : normalizeTextEdits(input.text, [...strictCombinedEdits, ...planningCleanup.edits], "Contract 10 planning-aware advance");
1937
- const combinedText = contract7Planned.updatedText === null
2028
+ : normalizeTextEdits(reviewBaseText, [...strictCombinedEdits, ...planningCleanup.edits], "Contract 10 planning-aware advance");
2029
+ const versionLiftOffset = input.text.indexOf(" version 10") + " version 9".length;
2030
+ const liftedEdits = grammar10
2031
+ ? Object.freeze(lowerCombinedEdits.map((edit) => Object.freeze({
2032
+ ...edit,
2033
+ startOffset: edit.startOffset >= versionLiftOffset ? edit.startOffset + 1 : edit.startOffset,
2034
+ endOffset: edit.endOffset >= versionLiftOffset ? edit.endOffset + 1 : edit.endOffset,
2035
+ })))
2036
+ : lowerCombinedEdits;
2037
+ const reviewCandidate = grammar10 && contract7Planned.updatedText !== null
2038
+ ? composePlanReviewAdvanceCandidate(input.text, liftedEdits, contract7Planned.advance?.removedTaskIds ?? [])
2039
+ : null;
2040
+ const reviewFailed = reviewCandidate !== null && !reviewCandidate.ok;
2041
+ const combinedEdits = reviewFailed ? Object.freeze([]) :
2042
+ reviewCandidate?.edits ?? liftedEdits;
2043
+ const combinedText = contract7Planned.updatedText === null ||
2044
+ reviewFailed
1938
2045
  ? null
1939
- : applyTextEdits(input.text, combinedEdits);
2046
+ : reviewCandidate?.updatedText ?? applyTextEdits(input.text, combinedEdits);
1940
2047
  const combinedCheck = combinedText === null
1941
2048
  ? null
1942
- : grammar9
2049
+ : grammar9 || grammar10
1943
2050
  ? checkDocument(combinedText, { maxDiagnostics })
1944
2051
  : checkContract8Document(combinedText, { maxDiagnostics });
1945
2052
  if (combinedCheck !== null && !combinedCheck.ok) {
@@ -1951,6 +2058,7 @@ async function runAdvance(args) {
1951
2058
  const planned = Object.freeze({
1952
2059
  ...contract7Planned,
1953
2060
  schemaVersion: "Perttool.AdvanceResult.v4",
2061
+ changed: reviewFailed ? false : contract7Planned.changed,
1954
2062
  originalDigest: input.digest,
1955
2063
  updatedText: combinedText,
1956
2064
  updatedDigest: combinedText === null ? null : sha256DigestUtf8(combinedText),
@@ -1959,7 +2067,11 @@ async function runAdvance(args) {
1959
2067
  updatedLabel: "candidate",
1960
2068
  }),
1961
2069
  edits: combinedEdits,
1962
- diagnostics: combinedDiagnostics?.diagnostics ?? contract7Planned.diagnostics,
2070
+ ok: contract7Planned.ok && !reviewFailed,
2071
+ diagnostics: Object.freeze([
2072
+ ...(combinedDiagnostics?.diagnostics ?? contract7Planned.diagnostics),
2073
+ ...(reviewCandidate?.diagnostics ?? []),
2074
+ ]),
1963
2075
  diagnosticsTruncated: contract7Planned.diagnosticsTruncated ||
1964
2076
  (combinedCheck?.diagnosticsTruncated ?? false) ||
1965
2077
  (combinedDiagnostics?.truncated ?? false),
@@ -1970,6 +2082,7 @@ async function runAdvance(args) {
1970
2082
  ...contract7Planned.advance,
1971
2083
  removedPlanningLinks: planningCleanup.removedPlanningLinks,
1972
2084
  archivedWorkIds: planningCleanup.archivedWorkIds,
2085
+ ...(grammar10 ? { removedPlanReviewRequestIds: reviewCandidate?.removedRequestIds ?? Object.freeze([]) } : {}),
1973
2086
  }),
1974
2087
  });
1975
2088
  const initialWarningFailure = parsed.flags.has("warnings-as-errors") &&
@@ -1988,14 +2101,17 @@ async function runAdvance(args) {
1988
2101
  warningDenied: initialWarningFailure,
1989
2102
  maxDiagnostics,
1990
2103
  documentValidator: (candidateText, candidateMaxDiagnostics) => {
1991
- const planningBase = grammar9
1992
- ? planningPoolBaseText(candidateText, scanPlanningDeclarationBlocks(candidateText))
2104
+ const reviewBase = /^ version 10$/mu.test(candidateText)
2105
+ ? planReviewBaseText(candidateText, scanPlanReviewDeclarationBlocks(candidateText))
1993
2106
  : candidateText;
2107
+ const planningBase = /^ version 9$/mu.test(reviewBase)
2108
+ ? planningPoolBaseText(reviewBase, scanPlanningDeclarationBlocks(reviewBase))
2109
+ : reviewBase;
1994
2110
  const acceptanceBase = /^ version 8$/mu.test(planningBase)
1995
2111
  ? temporalScheduleBaseText(planningBase, scanTemporalDeclarationBlocks(planningBase))
1996
2112
  : planningBase;
1997
2113
  const acceptanceSource = parseMilestoneAcceptanceSource(acceptanceBase, MILESTONE_ACCEPTANCE_SOURCE_CAPABILITY);
1998
- const checked = grammar9
2114
+ const checked = /^ version (?:9|10)$/mu.test(candidateText)
1999
2115
  ? checkDocument(candidateText, { maxDiagnostics: candidateMaxDiagnostics })
2000
2116
  : checkContract8Document(candidateText, { maxDiagnostics: candidateMaxDiagnostics });
2001
2117
  return {
@@ -2007,62 +2123,72 @@ async function runAdvance(args) {
2007
2123
  };
2008
2124
  },
2009
2125
  });
2010
- const planningAdvanceGuard = planningCleanup.destructiveRecords.length === 0
2126
+ const planningRecords = grammar10
2127
+ ? Object.freeze(planningCleanup.destructiveRecords.map((record) => Object.freeze({
2128
+ ...record,
2129
+ startOffset: record.startOffset >= versionLiftOffset ? record.startOffset + 1 : record.startOffset,
2130
+ endOffset: record.endOffset >= versionLiftOffset ? record.endOffset + 1 : record.endOffset,
2131
+ })))
2132
+ : planningCleanup.destructiveRecords;
2133
+ const needsReviewHistory = reviewCandidate?.ok === true &&
2134
+ reviewCandidate.destructiveRecords.length > 0;
2135
+ const captureAdditionalHistory = needsReviewHistory &&
2136
+ writeRequest.mode === "in_place" && preparedBase.baseline === null &&
2137
+ planned.ok && !initialWarningFailure &&
2138
+ (planned.governance?.writeAuthorized ?? false);
2139
+ const effectiveBaseline = captureAdditionalHistory
2140
+ ? await captureAdvanceHistoryBaseline({
2141
+ targetPath: sourceOperand, expectedSourceDigest: input.digest,
2142
+ })
2143
+ : preparedBase.baseline;
2144
+ const planningAdvanceGuard = planningRecords.length === 0
2145
+ ? planningHistoryNotApplicable([], "no_canonical_records")
2146
+ : writeRequest.mode === "preview"
2147
+ ? planningHistoryNotApplicable(planningRecords, "preview")
2148
+ : writeRequest.mode === "out"
2149
+ ? planningHistoryNotApplicable(planningRecords, "separate_output")
2150
+ : effectiveBaseline === null
2151
+ ? planningHistoryNotApplicable(planningRecords, "authority_denied")
2152
+ : assessPlanningHistoryBaseline(input.text, planningRecords, effectiveBaseline, forceRequested);
2153
+ const reviewAdvanceGuard = !needsReviewHistory
2011
2154
  ? planningHistoryNotApplicable([], "no_canonical_records")
2012
2155
  : writeRequest.mode === "preview"
2013
- ? planningHistoryNotApplicable(planningCleanup.destructiveRecords, "preview")
2156
+ ? planningHistoryNotApplicable(reviewCandidate.destructiveRecords, "preview")
2014
2157
  : writeRequest.mode === "out"
2015
- ? planningHistoryNotApplicable(planningCleanup.destructiveRecords, "separate_output")
2016
- : preparedBase.baseline === null
2017
- ? planningHistoryNotApplicable(planningCleanup.destructiveRecords, "authority_denied")
2018
- : assessPlanningHistoryBaseline(input.text, planningCleanup.destructiveRecords, preparedBase.baseline, forceRequested);
2019
- const planningAdvanceBlocked = planningAdvanceGuard.status === "blocked";
2158
+ ? planningHistoryNotApplicable(reviewCandidate.destructiveRecords, "separate_output")
2159
+ : effectiveBaseline === null
2160
+ ? planningHistoryNotApplicable(reviewCandidate.destructiveRecords, "authority_denied")
2161
+ : assessPlanReviewAdvanceHistory(input.text, reviewCandidate, effectiveBaseline, forceRequested);
2162
+ const outerAdvanceBlocked = planningAdvanceGuard.status === "blocked" ||
2163
+ reviewAdvanceGuard.status === "blocked";
2020
2164
  const baseHistoryGuard = preparedBase.result.historyGuard;
2021
2165
  const combinedHistoryGuard = baseHistoryGuard === null
2022
2166
  ? null
2023
- : planningCleanup.destructiveRecords.length === 0
2024
- ? baseHistoryGuard
2025
- : Object.freeze({
2026
- ...baseHistoryGuard,
2027
- status: planningAdvanceGuard.status === "blocked"
2028
- ? "blocked"
2029
- : planningAdvanceGuard.status === "forced"
2030
- ? "forced"
2031
- : baseHistoryGuard.status,
2032
- cause: planningAdvanceGuard.status === "passed" ||
2033
- planningAdvanceGuard.status === "not_applicable"
2034
- ? baseHistoryGuard.cause
2035
- : planningAdvanceGuard.cause,
2036
- destructiveEntityIds: Object.freeze([...new Set([
2037
- ...baseHistoryGuard.destructiveEntityIds,
2038
- ...planningAdvanceGuard.destructiveEntityIds,
2039
- ])].sort()),
2040
- overlappingEntityIds: Object.freeze([...new Set([
2041
- ...baseHistoryGuard.overlappingEntityIds,
2042
- ...planningAdvanceGuard.overlappingEntityIds,
2043
- ])].sort()),
2044
- });
2045
- const preparedDiagnostics = planningAdvanceBlocked
2167
+ : mergeOuterAdvanceHistoryGuards(baseHistoryGuard, [planningAdvanceGuard, reviewAdvanceGuard]);
2168
+ const preparedDiagnostics = outerAdvanceBlocked
2046
2169
  ? limitDiagnostics(sortDiagnostics([
2047
2170
  ...preparedBase.result.diagnostics,
2048
2171
  Object.freeze({
2049
2172
  code: "PTADV-101",
2050
2173
  severity: "error",
2051
- message: "advance history proof is blocked for canonical planning facts",
2174
+ message: "advance history proof is blocked for canonical planning or Plan Review facts",
2052
2175
  helpTopic: "editing",
2053
2176
  data: Object.freeze({
2054
- cause: planningAdvanceGuard.cause,
2055
- entity_ids: planningAdvanceGuard.overlappingEntityIds,
2177
+ cause: planningAdvanceGuard.status === "blocked"
2178
+ ? planningAdvanceGuard.cause : reviewAdvanceGuard.cause,
2179
+ entity_ids: [...planningAdvanceGuard.overlappingEntityIds,
2180
+ ...reviewAdvanceGuard.overlappingEntityIds],
2056
2181
  }),
2057
2182
  }),
2058
2183
  ]), maxDiagnostics)
2059
2184
  : null;
2060
2185
  const prepared = Object.freeze({
2061
2186
  ...preparedBase,
2187
+ baseline: effectiveBaseline,
2062
2188
  result: Object.freeze({
2063
2189
  ...preparedBase.result,
2064
2190
  schemaVersion: "Perttool.AdvanceResult.v4",
2065
- ok: preparedBase.result.ok && !planningAdvanceBlocked,
2191
+ ok: preparedBase.result.ok && !outerAdvanceBlocked,
2066
2192
  diagnostics: preparedDiagnostics?.diagnostics ?? preparedBase.result.diagnostics,
2067
2193
  diagnosticsTruncated: preparedBase.result.diagnosticsTruncated ||
2068
2194
  (preparedDiagnostics?.truncated ?? false),
@@ -3055,7 +3181,7 @@ async function runNext(args) {
3055
3181
  if (format === "json") {
3056
3182
  writeJson({
3057
3183
  schema_version: "Perttool.CliError.v1",
3058
- cli_contract_version: 9,
3184
+ cli_contract_version: 11,
3059
3185
  tool_version: TOOL_VERSION,
3060
3186
  operation: "dag.next",
3061
3187
  ok: false,
@@ -3074,28 +3200,33 @@ async function runNext(args) {
3074
3200
  result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
3075
3201
  const ok = result.ok && !warningFailure;
3076
3202
  if (format === "json") {
3077
- writeJson(liftContract9NextResultJson({
3078
- schema_version: "Perttool.NextResult.v7",
3079
- cli_contract_version: 8,
3080
- recommendation_interface_version: 1,
3081
- tool_version: TOOL_VERSION,
3082
- operation: "dag.next",
3083
- ok,
3084
- document_id: result.documentId,
3085
- source,
3086
- source_digest: input.digest,
3087
- diagnostics: result.diagnostics.map(jsonDiagnostic),
3088
- diagnostics_truncated: result.diagnosticsTruncated,
3089
- grammar_version: result.grammarVersion,
3090
- ...(result.durationUnit === null ? {} : nextJson(result)),
3091
- temporal: snakeJson(result.temporal),
3092
- assurance: contract7SnakeJson(result.assurance),
3093
- acceptance: snakeJson(result.acceptance),
3094
- }, result));
3203
+ writeJson({ plan_review: planReviewProjection(input.text), ...liftContract9NextResultJson({
3204
+ schema_version: "Perttool.NextResult.v7",
3205
+ cli_contract_version: 8,
3206
+ recommendation_interface_version: 1,
3207
+ tool_version: TOOL_VERSION,
3208
+ operation: "dag.next",
3209
+ ok,
3210
+ document_id: result.documentId,
3211
+ source,
3212
+ source_digest: input.digest,
3213
+ diagnostics: result.diagnostics.map(jsonDiagnostic),
3214
+ diagnostics_truncated: result.diagnosticsTruncated,
3215
+ grammar_version: result.grammarVersion,
3216
+ ...(result.durationUnit === null ? {} : nextJson(result)),
3217
+ temporal: snakeJson(result.temporal),
3218
+ assurance: contract7SnakeJson(result.assurance),
3219
+ acceptance: snakeJson(result.acceptance),
3220
+ }, result), schema_version: "Perttool.NextResult.v9" });
3095
3221
  }
3096
3222
  else {
3097
- if (ok)
3223
+ if (ok) {
3224
+ const review = planReviewProjection(input.text);
3225
+ if (review !== null) {
3226
+ process.stdout.write(`PLAN_REVIEW ${JSON.stringify(review)}\n`);
3227
+ }
3098
3228
  process.stdout.write(renderNextText(result));
3229
+ }
3099
3230
  for (const diagnostic of result.diagnostics) {
3100
3231
  process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
3101
3232
  }
@@ -3284,16 +3415,12 @@ async function runMilestoneAcceptance(action, args) {
3284
3415
  return result.ok ? 0 : 1;
3285
3416
  }
3286
3417
  async function runDocumentMigration(args) {
3287
- const target = args.includes("--target-grammar")
3288
- ? args[args.indexOf("--target-grammar") + 1]
3289
- : args.find((value) => value.startsWith("--target-grammar="))?.slice("--target-grammar=".length);
3290
- if (target === "8")
3291
- return runContract9DocumentMigration(args);
3292
- if (target === "9")
3293
- return runContract10DocumentMigration(args);
3418
+ const supported = await runSupportedGrammarMigration(args);
3419
+ if (supported !== null)
3420
+ return supported;
3294
3421
  const parsed = parseCommandOptions("document.migrate", args);
3295
3422
  if (parsed.positionals.length !== 1 || parsed.values.get("target-grammar") !== "7")
3296
- throw new UsageError("document migrate requires <file> --target-grammar 7, 8, or 9");
3423
+ throw new UsageError("document migrate requires <file> --target-grammar 7, 8, 9, or 10");
3297
3424
  const sourceOperand = parsed.positionals[0];
3298
3425
  if (sourceOperand === "-")
3299
3426
  throw new UsageError("document migrate requires a repository file");
@@ -3955,6 +4082,229 @@ function planningHistoryDiagnostic(guard) {
3955
4082
  }),
3956
4083
  });
3957
4084
  }
4085
+ function planReviewRequestJson(request, sourceDigest) {
4086
+ return Object.freeze({
4087
+ id: request.id, qualified_id: request.qualifiedId,
4088
+ task_id: request.taskId, qualified_task_id: request.qualifiedTaskId,
4089
+ state: request.outcome === null ? "open" : "resolved",
4090
+ task_reference_state: request.taskReferenceState, model: request.model,
4091
+ reason: request.reason, created_at: request.createdAt,
4092
+ created_by: request.createdBy, locator: request.locator,
4093
+ outcome: request.outcome, resolved_at: request.resolvedAt,
4094
+ resolved_by: request.resolvedBy, resolution_reason: request.resolutionReason,
4095
+ reviewed_source_digest: request.reviewedSourceDigest,
4096
+ change_request_digest: request.changeRequestDigest,
4097
+ plan_basis_before: request.planBasisBefore,
4098
+ plan_basis_after: request.planBasisAfter,
4099
+ source_binding: Object.freeze({ source_digest: sourceDigest, span: jsonSpan(request.span) }),
4100
+ });
4101
+ }
4102
+ function planReviewCliDiagnostic(code, message) {
4103
+ return Object.freeze({ code, severity: "error", message,
4104
+ helpTopic: "plan-review", data: Object.freeze({}) });
4105
+ }
4106
+ async function planReviewCommandInput(operation, args) {
4107
+ const parsed = parseCommandOptions(operation, args);
4108
+ const sourceOperand = parsed.positionals[0];
4109
+ return Object.freeze({
4110
+ parsed, sourceOperand,
4111
+ source: sourceOperand === "-" ? "<stdin>" : sourceOperand,
4112
+ format: outputFormat(parsed.values.get("format")),
4113
+ input: await readDocument(sourceOperand),
4114
+ });
4115
+ }
4116
+ function planReviewInputFailure(error, operation, args) {
4117
+ if (error instanceof UsageError)
4118
+ throw error;
4119
+ return cliError(error instanceof Error ? error : new Error(String(error)), 3, operation, jsonRequested(args));
4120
+ }
4121
+ async function runPlanReviewRead(action, args) {
4122
+ const operation = `plan.${action}`;
4123
+ let commandInput;
4124
+ try {
4125
+ commandInput = await planReviewCommandInput(operation, args);
4126
+ }
4127
+ catch (error) {
4128
+ return planReviewInputFailure(error, operation, args);
4129
+ }
4130
+ const { parsed, format, source, input } = commandInput;
4131
+ const result = parsePlanReviewSource(input.text, PLAN_REVIEW_SOURCE_CAPABILITY);
4132
+ const state = action === "review-list" ? parsed.values.get("state") ?? "all" : null;
4133
+ if (state !== null && state !== "open" && state !== "resolved" && state !== "all") {
4134
+ throw new UsageError("--state must be open, resolved, or all");
4135
+ }
4136
+ const id = action === "review-show" ? parsed.positionals[1] : null;
4137
+ const matched = result.model?.requests.filter((request) => (id === null || request.id === id) &&
4138
+ (state === null || state === "all" ||
4139
+ (state === "open" ? request.outcome === null : request.outcome !== null))) ?? [];
4140
+ const diagnostics = [
4141
+ ...result.diagnostics,
4142
+ ...(result.grammarVersion !== 10 ? [planReviewCliDiagnostic("PTREV-110", "Plan Review requires Grammar 10; migrate this document first")] : []),
4143
+ ...(id !== null && result.ok && matched.length === 0 ? [planReviewCliDiagnostic("PTREV-105", `Plan Review Request ${id} was not found`)] : []),
4144
+ ];
4145
+ const ok = result.ok && result.model !== null &&
4146
+ !diagnostics.some(({ severity }) => severity === "error");
4147
+ const payload = Object.freeze({
4148
+ schema_version: "Perttool.PlanReviewResult.v1", cli_contract_version: 11,
4149
+ tool_version: TOOL_VERSION, operation, ok, document_id: result.documentId,
4150
+ source, query: Object.freeze({ state, request_id: id }),
4151
+ projection: result.model === null ? null : projectPlanReviewState(result.model),
4152
+ requests: matched.map((request) => planReviewRequestJson(request, input.digest)),
4153
+ diagnostics: diagnostics.map(jsonDiagnostic),
4154
+ });
4155
+ if (format === "json")
4156
+ writeJson(payload);
4157
+ else if (ok) {
4158
+ for (const request of matched) {
4159
+ process.stdout.write(`${request.id} ${request.taskId} ${request.outcome === null ? "open" : request.outcome}: ${request.reason}\n`);
4160
+ }
4161
+ }
4162
+ else
4163
+ for (const diagnostic of diagnostics) {
4164
+ process.stderr.write(`${renderDiagnostic(diagnostic, source, "never")}\n`);
4165
+ }
4166
+ return ok ? 0 : 1;
4167
+ }
4168
+ function planReviewWriteRequest(parsed, sourceOperand) {
4169
+ const inPlace = parsed.flags.has("in-place");
4170
+ const output = parsed.values.get("output");
4171
+ const expectedDigest = parsed.values.get("expected-digest");
4172
+ if (inPlace && output !== undefined)
4173
+ throw new UsageError("--output and --in-place are mutually exclusive");
4174
+ if (parsed.flags.has("preview") && (inPlace || output !== undefined)) {
4175
+ throw new UsageError("--preview cannot be combined with a write target");
4176
+ }
4177
+ if (inPlace && sourceOperand === "-")
4178
+ throw new UsageError("--in-place requires a file");
4179
+ if (expectedDigest !== undefined && !/^sha256:[0-9a-f]{64}$/.test(expectedDigest)) {
4180
+ throw new UsageError("--expected-digest requires a lowercase SHA-256 digest");
4181
+ }
4182
+ if (inPlace)
4183
+ return Object.freeze({ mode: "in_place", target: sourceOperand,
4184
+ ...(expectedDigest === undefined ? {} : { expectedDigest }) });
4185
+ if (output !== undefined)
4186
+ return Object.freeze({ mode: "out", target: output });
4187
+ return Object.freeze({ mode: "preview", target: null });
4188
+ }
4189
+ async function runPlanReviewMutation(action, args) {
4190
+ const operation = `plan.${action}`;
4191
+ let commandInput;
4192
+ try {
4193
+ commandInput = await planReviewCommandInput(operation, args);
4194
+ }
4195
+ catch (error) {
4196
+ return planReviewInputFailure(error, operation, args);
4197
+ }
4198
+ const { parsed, format, sourceOperand, source, input } = commandInput;
4199
+ const writeRequest = planReviewWriteRequest(parsed, sourceOperand);
4200
+ if (writeRequest.mode === "in_place" && writeRequest.expectedDigest !== undefined &&
4201
+ writeRequest.expectedDigest !== input.digest) {
4202
+ return writeFailureExit(new SafeWriteConflictError("expected_digest_mismatch", "--expected-digest does not match the initial document digest"), operation, format === "json");
4203
+ }
4204
+ const requestPath = parsed.values.get("request");
4205
+ let batchRequest;
4206
+ if (requestPath !== undefined) {
4207
+ if (sourceOperand === "-" && requestPath === "-") {
4208
+ throw new UsageError("document and JSON request cannot both use stdin");
4209
+ }
4210
+ try {
4211
+ batchRequest = await readPlanReviewBatchRequest(requestPath);
4212
+ }
4213
+ catch (error) {
4214
+ if (error instanceof PlanReviewBatchTooLarge) {
4215
+ batchRequest = null;
4216
+ }
4217
+ else {
4218
+ if (error instanceof UsageError)
4219
+ throw error;
4220
+ return cliError(error instanceof Error ? error : new Error(String(error)), 3, operation, format === "json");
4221
+ }
4222
+ }
4223
+ }
4224
+ const requestInput = action === "review-request"
4225
+ ? Object.freeze({ schemaVersion: PLAN_REVIEW_CREATE_REQUEST_ID,
4226
+ requestId: parsed.positionals[1], taskId: parsed.positionals[2],
4227
+ reason: parsed.values.get("reason"), createdAt: parsed.values.get("created-at"),
4228
+ actor: parsed.values.get("actor"),
4229
+ ...(parsed.values.get("locator") === undefined ? {} : { locator: parsed.values.get("locator") }) })
4230
+ : Object.freeze({ schemaVersion: PLAN_REVIEW_RESOLVE_REQUEST_ID,
4231
+ requestId: parsed.positionals[1], outcome: parsed.values.get("outcome"),
4232
+ resolvedAt: parsed.values.get("resolved-at"), actor: parsed.values.get("actor"),
4233
+ resolutionReason: parsed.values.get("resolution-reason"),
4234
+ acceptedOwners: Object.freeze(parsed.repeatedValues.get("accepted-owner") ?? []),
4235
+ ...(requestPath === undefined ? {} : { request: batchRequest }), });
4236
+ const normalizedCreate = action === "review-request"
4237
+ ? normalizePlanReviewCreateRequest(requestInput) : null;
4238
+ const normalizedResolve = action === "review-resolve"
4239
+ ? normalizePlanReviewResolveRequest(requestInput) : null;
4240
+ const expectedDigest = parsed.values.get("expected-digest");
4241
+ const mutationOptions = expectedDigest === undefined ? {} : { expectedDigest };
4242
+ const result = action === "review-request"
4243
+ ? planPlanReviewCreate(input.text, requestInput, mutationOptions)
4244
+ : planPlanReviewResolve(input.text, requestInput, mutationOptions);
4245
+ let write = Object.freeze({
4246
+ mode: writeRequest.mode, target: writeRequest.target, written: false,
4247
+ });
4248
+ if (result.ok && result.changed && writeRequest.mode !== "preview") {
4249
+ try {
4250
+ write = await persistPlanReviewMutation(result, writeRequest.mode === "in_place"
4251
+ ? { mode: "in_place", target: writeRequest.target,
4252
+ ...(writeRequest.expectedDigest === undefined ? {} : { expectedDigest: writeRequest.expectedDigest }) }
4253
+ : { mode: "out", source: sourceOperand, target: writeRequest.target }, createNodeHost().safePersistence);
4254
+ }
4255
+ catch (error) {
4256
+ return writeFailureExit(error, operation, format === "json");
4257
+ }
4258
+ }
4259
+ const requestWire = normalizedCreate?.request !== undefined && normalizedCreate.request !== null
4260
+ ? snakeJson(normalizedCreate.request)
4261
+ : normalizedResolve?.request === undefined || normalizedResolve.request === null ? null
4262
+ : Object.freeze({ schema_version: PLAN_REVIEW_RESOLVE_REQUEST_ID,
4263
+ request_id: normalizedResolve.request.requestId,
4264
+ outcome: normalizedResolve.request.outcome, resolved_at: normalizedResolve.request.resolvedAt,
4265
+ actor: normalizedResolve.request.actor, resolution_reason: normalizedResolve.request.resolutionReason,
4266
+ accepted_owners: normalizedResolve.request.acceptedOwners,
4267
+ change_request_digest: normalizedResolve.request.changeRequestDigest });
4268
+ const requestBefore = result.requestBefore === null ? null
4269
+ : planReviewRequestJson(result.requestBefore, input.digest);
4270
+ const requestAfter = result.requestAfter === null ? null
4271
+ : planReviewRequestJson(result.requestAfter, result.updatedDigest ?? input.digest);
4272
+ const composed = result.composedMutation === null ? null : Object.freeze({
4273
+ ok: result.composedMutation.ok, changed: result.composedMutation.changed,
4274
+ original_digest: result.composedMutation.originalDigest,
4275
+ updated_digest: result.composedMutation.updatedDigest,
4276
+ updated_text: result.composedMutation.updatedText,
4277
+ edits: snakeJson(result.composedMutation.edits),
4278
+ diagnostics: result.composedMutation.diagnostics.map(jsonDiagnostic),
4279
+ diagnostics_truncated: result.composedMutation.diagnosticsTruncated,
4280
+ });
4281
+ const payload = Object.freeze({
4282
+ schema_version: "Perttool.PlanReviewMutationResult.v1", cli_contract_version: 11,
4283
+ tool_version: TOOL_VERSION, operation, ok: result.ok,
4284
+ document_id: result.documentId, source, request: requestWire,
4285
+ request_before: requestBefore, request_after: requestAfter,
4286
+ projection_before: result.projectionBefore, projection_after: result.projectionAfter,
4287
+ plan_basis: snakeJson(result.planBasis),
4288
+ plan_review_authority: result.planReviewAuthority,
4289
+ composed_mutation: composed,
4290
+ candidate: Object.freeze({ source_digest: input.digest,
4291
+ candidate_digest: result.updatedDigest, text: result.updatedText,
4292
+ changed: result.changed }),
4293
+ edits: snakeJson(result.edits), diff: result.diff, write,
4294
+ diagnostics: result.diagnostics.map(jsonDiagnostic),
4295
+ });
4296
+ if (format === "json")
4297
+ writeJson(payload);
4298
+ else if (result.ok && result.updatedText !== null) {
4299
+ process.stdout.write(parsed.flags.has("diff") ? result.diff ?? "" : result.updatedText);
4300
+ }
4301
+ else
4302
+ for (const diagnostic of result.diagnostics) {
4303
+ process.stderr.write(`${renderDiagnostic(diagnostic, source, "never")}\n`);
4304
+ }
4305
+ return result.ok ? 0
4306
+ : result.diagnostics.some(({ code, data }) => code === "PTREV-106" && data?.["cause"] === "expected_digest_mismatch") ? 5 : 1;
4307
+ }
3958
4308
  async function runPlanningReshapeApply(args) {
3959
4309
  const operation = "work.reshape.apply";
3960
4310
  const parsed = parseCommandOptions(operation, args);
@@ -4153,6 +4503,14 @@ async function dispatchCommand(descriptor, args) {
4153
4503
  return runRender(args);
4154
4504
  case "dag.history":
4155
4505
  return runHistoricalGraph(args);
4506
+ case "plan.review-list":
4507
+ return runPlanReviewRead("review-list", args);
4508
+ case "plan.review-show":
4509
+ return runPlanReviewRead("review-show", args);
4510
+ case "plan.review-request":
4511
+ return runPlanReviewMutation("review-request", args);
4512
+ case "plan.review-resolve":
4513
+ return runPlanReviewMutation("review-resolve", args);
4156
4514
  case "dag.import":
4157
4515
  return runImport(args);
4158
4516
  case "calendar.add":
@@ -4215,7 +4573,7 @@ async function dispatchCommand(descriptor, args) {
4215
4573
  const [resource, action] = descriptor.path;
4216
4574
  return runMutation(resource, action, args);
4217
4575
  }
4218
- throw new Error(`no Contract 10 handler for ${descriptor.operation}`);
4576
+ throw new Error(`no Contract 11 handler for ${descriptor.operation}`);
4219
4577
  }
4220
4578
  function emitCommandUsage(error, json) {
4221
4579
  if (json) {