@unbrained/pm-cli 2026.10.28 → 2026.10.30

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 (166) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/cli/commands/lifecycle/append.d.ts +4 -0
  4. package/dist/cli/commands/lifecycle/append.js +1 -1
  5. package/dist/cli/commands/lifecycle/create.d.ts +4 -0
  6. package/dist/cli/commands/lifecycle/create.js +1 -1
  7. package/dist/cli/commands/lifecycle/plan.d.ts +3 -0
  8. package/dist/cli/commands/lifecycle/plan.js +1 -1
  9. package/dist/cli/commands/lifecycle/update-many.d.ts +4 -0
  10. package/dist/cli/commands/lifecycle/update-many.js +1 -1
  11. package/dist/cli/commands/lifecycle/update.d.ts +4 -0
  12. package/dist/cli/commands/lifecycle/update.js +1 -1
  13. package/dist/cli/help-content.js +2 -2
  14. package/dist/cli/register-list-query.js +3 -3
  15. package/dist/cli-bundle/bundle-manifest.json +1 -1
  16. package/dist/cli-bundle/chunks/{chunk-XHONSCLJ.js → chunk-34MP7WKK.js} +3 -3
  17. package/dist/cli-bundle/chunks/chunk-42BHHNTL.js +128 -0
  18. package/dist/cli-bundle/chunks/{chunk-D6P6IBPN.js → chunk-4BQD5SW2.js} +46 -46
  19. package/dist/cli-bundle/chunks/{chunk-YYD5MNAV.js → chunk-EHBGHX55.js} +2 -2
  20. package/dist/cli-bundle/chunks/{chunk-DM3PICGU.js → chunk-GRABKNFL.js} +2 -2
  21. package/dist/cli-bundle/chunks/chunk-I5O3XZOA.js +2 -0
  22. package/dist/cli-bundle/chunks/{chunk-CXUQEZDX.js → chunk-LEIVUGDH.js} +2 -2
  23. package/dist/cli-bundle/chunks/chunk-MVL27TAH.js +2 -0
  24. package/dist/cli-bundle/chunks/{chunk-7V37RC2L.js → chunk-OH7WPD42.js} +35 -35
  25. package/dist/cli-bundle/chunks/chunk-OOD2A7AF.js +20 -0
  26. package/dist/cli-bundle/chunks/chunk-Q5V7P6NM.js +2 -0
  27. package/dist/cli-bundle/chunks/{chunk-2J7I54MQ.js → chunk-QLPFYBXN.js} +2 -2
  28. package/dist/cli-bundle/chunks/chunk-S2U7ECUJ.js +2 -0
  29. package/dist/cli-bundle/chunks/{chunk-E5QBZ4HI.js → chunk-SA5NYLPE.js} +2 -2
  30. package/dist/cli-bundle/chunks/{chunk-YUBUNECD.js → chunk-UUQX3PFD.js} +57 -57
  31. package/dist/cli-bundle/chunks/chunk-VHLAVTJN.js +31 -0
  32. package/dist/cli-bundle/chunks/{chunk-DKLEDWVW.js → chunk-XAOVJG52.js} +2 -2
  33. package/dist/cli-bundle/chunks/chunk-XPQ4RTSF.js +2 -0
  34. package/dist/cli-bundle/chunks/{chunk-D3AIQZXF.js → chunk-Y6NC3X3E.js} +2 -2
  35. package/dist/cli-bundle/chunks/item-transaction-WIOBZW4E.js +2 -0
  36. package/dist/cli-bundle/chunks/{mutation-admission-GKDKHRTZ.js → mutation-admission-TRGAVS7N.js} +2 -2
  37. package/dist/cli-bundle/chunks/{register-list-query-MHJJUM6M.js → register-list-query-BH4F6M7Z.js} +6 -6
  38. package/dist/cli-bundle/chunks/register-mutation-5V2NANIS.js +20 -0
  39. package/dist/cli-bundle/chunks/register-operations-XSFRMUFX.js +2 -0
  40. package/dist/cli-bundle/chunks/{register-setup-WUZ2ZSMY.js → register-setup-QPWSVHFZ.js} +2 -2
  41. package/dist/cli-bundle/chunks/{stage-2FYJGGG3.js → stage-RTB6RAAO.js} +2 -2
  42. package/dist/cli-bundle/focused-chunks/chunk-2V6NMNYN.js +26 -0
  43. package/dist/cli-bundle/focused-chunks/{chunk-ML5JIUM6.js → chunk-3SRX77ES.js} +2 -2
  44. package/dist/cli-bundle/focused-chunks/{chunk-YLVHHWHN.js → chunk-4PW5XWVI.js} +2 -2
  45. package/dist/cli-bundle/focused-chunks/{chunk-Z26ZPQH6.js → chunk-ASHGXMX4.js} +3 -3
  46. package/dist/cli-bundle/focused-chunks/{chunk-JTCGDODH.js → chunk-BCIUAEWX.js} +2 -2
  47. package/dist/cli-bundle/focused-chunks/{chunk-7SZPZOIC.js → chunk-BPE6JTKL.js} +2 -2
  48. package/dist/cli-bundle/focused-chunks/{chunk-LPWZERVD.js → chunk-CKBN2D42.js} +3 -3
  49. package/dist/cli-bundle/focused-chunks/chunk-FRFNGMAK.js +2 -0
  50. package/dist/cli-bundle/focused-chunks/chunk-I642DXOT.js +22 -0
  51. package/dist/cli-bundle/focused-chunks/chunk-K7DKG5HM.js +91 -0
  52. package/dist/cli-bundle/focused-chunks/chunk-NMQBXU7N.js +44 -0
  53. package/dist/cli-bundle/focused-chunks/{chunk-6VDRAJYX.js → chunk-PGM3ENGZ.js} +2 -2
  54. package/dist/cli-bundle/focused-chunks/{chunk-DRN4W3YJ.js → chunk-POJPJX64.js} +2 -2
  55. package/dist/cli-bundle/focused-chunks/{chunk-B4ZQJEMT.js → chunk-QFNV4JTA.js} +2 -2
  56. package/dist/cli-bundle/focused-chunks/{chunk-QUPGBO2L.js → chunk-QVJCQMZY.js} +2 -2
  57. package/dist/cli-bundle/focused-chunks/chunk-R2TPPPVU.js +2 -0
  58. package/dist/cli-bundle/focused-chunks/{chunk-TUN4M757.js → chunk-RGQEJFXO.js} +2 -2
  59. package/dist/cli-bundle/focused-chunks/{chunk-ON5WFDCG.js → chunk-RKPUQDWF.js} +2 -2
  60. package/dist/cli-bundle/focused-chunks/chunk-RYEZSZJ7.js +2 -0
  61. package/dist/cli-bundle/focused-chunks/{chunk-4GN2ZPT5.js → chunk-S23ZUASN.js} +2 -2
  62. package/dist/cli-bundle/focused-chunks/{chunk-R3EA3SDW.js → chunk-SAT7IAEB.js} +3 -3
  63. package/dist/cli-bundle/focused-chunks/{chunk-I4X6AKOO.js → chunk-VFWCSQAG.js} +2 -2
  64. package/dist/cli-bundle/focused-chunks/chunk-VJVJ2ZX2.js +2 -0
  65. package/dist/cli-bundle/focused-chunks/{chunk-C2GLO7HU.js → chunk-YQUZL2CK.js} +54 -54
  66. package/dist/cli-bundle/focused-chunks/{chunk-IQKLJFJE.js → chunk-YU7QOYYV.js} +18 -18
  67. package/dist/cli-bundle/focused-chunks/{indexed-receipt-blobs-5H6RMRTK.js → indexed-receipt-blobs-VK2363JB.js} +2 -2
  68. package/dist/cli-bundle/focused-chunks/item-transaction-ZFDKJIP7.js +2 -0
  69. package/dist/cli-bundle/focused-chunks/{mutation-admission-TM3SVU2O.js → mutation-admission-7VUODEPJ.js} +2 -2
  70. package/dist/cli-bundle/focused-chunks/{settlement-hook-LBVWWSCF.js → settlement-hook-OK6A7PG2.js} +2 -2
  71. package/dist/cli-bundle/focused-chunks/{stage-7ZBRX2Q2.js → stage-S5RP5SZH.js} +2 -2
  72. package/dist/cli-bundle/main.js +3 -3
  73. package/dist/cli-bundle/sdk-authoring.js +1 -1
  74. package/dist/cli-bundle/sdk-contracts.js +2 -2
  75. package/dist/cli-bundle/sdk-core.js +1 -43
  76. package/dist/cli-bundle/sdk-governance.js +1 -1
  77. package/dist/cli-bundle/sdk-graph.js +1 -1
  78. package/dist/cli-bundle/sdk-merge.js +61 -61
  79. package/dist/cli-bundle/sdk-query.js +1 -1
  80. package/dist/cli-bundle/sdk-runtime.js +1 -1
  81. package/dist/cli-bundle/sdk-testing.js +1 -1
  82. package/dist/cli-bundle/sdk.js +9 -9
  83. package/dist/core/history/operation-contract.d.ts +2 -2
  84. package/dist/core/history/operation-contract.js +1 -1
  85. package/dist/core/item/literal-mutation-input.d.ts +12 -0
  86. package/dist/core/item/literal-mutation-input.js +2 -0
  87. package/dist/core/item/parse.d.ts +2 -0
  88. package/dist/core/item/parse.js +2 -2
  89. package/dist/core/output/output.js +2 -2
  90. package/dist/core/store/item-store.js +1 -1
  91. package/dist/sdk/cli-contracts/flag-contracts.js +1 -1
  92. package/dist/sdk/cli-contracts/flag-lexicon-contracts.js +1 -1
  93. package/dist/sdk/cli-contracts/registration-helpers.js +1 -1
  94. package/dist/sdk/cli-contracts/tool-parameter-tables.js +1 -1
  95. package/dist/sdk/cli-contracts/tool-schema.js +1 -1
  96. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
  97. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +1 -1
  98. package/dist/sdk/item-transaction.d.ts +33 -2
  99. package/dist/sdk/item-transaction.js +1 -1
  100. package/dist/sdk/lifecycle/append.js +1 -1
  101. package/dist/sdk/lifecycle/cli-input.d.ts +4 -0
  102. package/dist/sdk/lifecycle/cli-input.js +2 -0
  103. package/dist/sdk/lifecycle/create.js +1 -1
  104. package/dist/sdk/lifecycle/legacy-none-tokens.js +1 -1
  105. package/dist/sdk/lifecycle/reopen.js +1 -1
  106. package/dist/sdk/lifecycle/update-many.js +1 -1
  107. package/dist/sdk/lifecycle/update.js +2 -2
  108. package/dist/sdk/output/table-rows.d.ts +9 -1
  109. package/dist/sdk/output/table-rows.js +1 -1
  110. package/dist/sdk/public-surface.json +1 -1
  111. package/dist/sdk/query/history.d.ts +2 -0
  112. package/dist/sdk/query/history.js +1 -1
  113. package/dist/sdk/runtime/mutation-value-validation.d.ts +16 -0
  114. package/dist/sdk/runtime/mutation-value-validation.js +2 -0
  115. package/dist/sdk/runtime-input.js +1 -1
  116. package/dist/sdk/runtime-primitives.d.ts +1 -0
  117. package/dist/sdk/runtime-primitives.js +1 -1
  118. package/dist/sdk/runtime.d.ts +14 -0
  119. package/dist/sdk/runtime.js +1 -1
  120. package/dist/sdk/structured-mutations.js +1 -1
  121. package/dist/sdk/workspace-transaction-gc.js +1 -1
  122. package/dist/sdk/workspace-transaction.d.ts +54 -0
  123. package/dist/sdk/workspace-transaction.js +2 -2
  124. package/docs/COMMANDS.md +15 -10
  125. package/docs/OUTPUT_PROJECTION_CONTRACTS.md +24 -2
  126. package/docs/SDK.md +134 -9
  127. package/docs/TESTING.md +8 -0
  128. package/docs/generated/FLAG_LEXICON_BUDGETS.md +1 -1
  129. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +6 -3
  130. package/marketplace.json +1 -1
  131. package/package.json +1 -1
  132. package/packages/pm-beads/package.json +1 -1
  133. package/packages/pm-calendar/package.json +1 -1
  134. package/packages/pm-command-kit/package.json +1 -1
  135. package/packages/pm-digital-twin/package.json +1 -1
  136. package/packages/pm-governance-audit/package.json +1 -1
  137. package/packages/pm-guide-shell/package.json +1 -1
  138. package/packages/pm-kanban/package.json +1 -1
  139. package/packages/pm-lifecycle-hooks/package.json +1 -1
  140. package/packages/pm-linked-test-adapters/package.json +1 -1
  141. package/packages/pm-search-advanced/package.json +1 -1
  142. package/packages/pm-templates/package.json +1 -1
  143. package/packages/pm-todos/package.json +1 -1
  144. package/packages/pm-vcs/package.json +1 -1
  145. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  146. package/plugins/pm-claude/package.json +1 -1
  147. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  148. package/plugins/pm-codex/package.json +1 -1
  149. package/plugins/pm-codex/plugin.json +1 -1
  150. package/runtime-dependencies.json +1 -1
  151. package/dist/cli-bundle/chunks/chunk-4D25HF3V.js +0 -2
  152. package/dist/cli-bundle/chunks/chunk-AHVN4GHF.js +0 -31
  153. package/dist/cli-bundle/chunks/chunk-CS7QDSUX.js +0 -128
  154. package/dist/cli-bundle/chunks/chunk-JESLOJD7.js +0 -2
  155. package/dist/cli-bundle/chunks/chunk-PQLRKFPZ.js +0 -20
  156. package/dist/cli-bundle/chunks/chunk-SGFQQXFD.js +0 -2
  157. package/dist/cli-bundle/chunks/chunk-ZX3XMTZG.js +0 -2
  158. package/dist/cli-bundle/chunks/register-mutation-SN425O3R.js +0 -20
  159. package/dist/cli-bundle/chunks/register-operations-EYUIUGPW.js +0 -2
  160. package/dist/cli-bundle/focused-chunks/chunk-3EHVI673.js +0 -2
  161. package/dist/cli-bundle/focused-chunks/chunk-46WLWK73.js +0 -2
  162. package/dist/cli-bundle/focused-chunks/chunk-6L4KKRK7.js +0 -26
  163. package/dist/cli-bundle/focused-chunks/chunk-BZSACRVX.js +0 -91
  164. package/dist/cli-bundle/focused-chunks/chunk-F6UQJJYB.js +0 -2
  165. package/dist/cli-bundle/focused-chunks/chunk-L5GX2FGZ.js +0 -2
  166. package/dist/cli-bundle/focused-chunks/chunk-OASYSUOH.js +0 -22
@@ -1,2 +1,2 @@
1
- (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7b430d12-7a5b-52c2-9a6b-e4c33a39da76")}catch{}})();import crypto from"node:crypto";import{ITEM_MUTATION_FACET_OPTION_MAPPINGS}from"./lifecycle/mutation-facet-contracts.js";import{normalizeItemId,normalizePrefix}from"../core/item/id.js";import{preserveMutationStdinTokenLiterals}from"../core/item/parse.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{levenshteinDistanceWithinLimit}from"../core/shared/levenshtein.js";import{describeUnknownError}from"./error-runtime.js";import{CLOSE_FLAG_CONTRACTS,RELEASE_FLAG_CONTRACTS}from"./cli-contracts/flag-contracts.js";import{CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS,UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS}from"./cli-contracts/commander-mutation-options.js";import{runtimeFieldOptionTarget}from"../core/schema/runtime-schema.js";import{CORE_ITEM_FIELD_KEYS as ITEM_FIELD_KEYS,resolveCoreUpsertMetadataKey}from"./lifecycle/core-item-fields.js";import{readRuntimeFieldOptionValue}from"../core/schema/runtime-field-values.js";import{assertMutationOptionValueTypes}from"./lifecycle/mutation-option-validation.js";const MUTATION_ROW_KEYS=["op","id","reason","options","unique","match"],REFERENCED_MUTATION_ROW_KEYS=[...MUTATION_ROW_KEYS,"ref"],DOCUMENT_MUTATION_ROW_KEYS=[...REFERENCED_MUTATION_ROW_KEYS,"item"],DERIVED_TRANSACTION_ID_PREFIX="mutate-",SYNTHESIZED_REFERENCE_PREFIX="row-",MUTATION_DOCUMENT_KEYS=["schema_version","mutations"],MUTATION_REFERENCE_SOURCE="[a-z][a-z0-9._-]{0,63}",MUTATION_REFERENCE_PATTERN=new RegExp(`^${MUTATION_REFERENCE_SOURCE}$`,"u"),MUTATION_DEPENDENCY_REFERENCE_PATTERN=new RegExp(`(^|,)id=@(${MUTATION_REFERENCE_SOURCE})(?=,|$)`,"gu"),flagToOptionKey=flag=>flag.slice(2).replaceAll(/-([a-z])/gu,(_match,letter)=>letter.toUpperCase()),ITEM_ENVELOPE_KEYS=["item","linked","schedule","claim_state","children","omission_receipt","row_contract","context_intent"],READ_ONLY_ITEM_KEYS=new Set(["created_at","updated_at","closed_at","version","format_version","path","author","collection_counts","notes_count","tests_count"]),UPDATE_READ_ONLY_ITEM_KEYS=new Set([...READ_ONLY_ITEM_KEYS,"completed_at","id"]),MUTATION_OPTION_KEYS={create:CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),update:UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),close:CLOSE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag)).filter(key=>key!=="reason"),release:RELEASE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag))};function deriveMutationTransactionId(input){const digest=crypto.createHash("sha256").update(input).digest("hex");return`${DERIVED_TRANSACTION_ID_PREFIX}${digest.slice(0,24)}`}function parseAtomicMutationControls(value){const controls={},compensation=value.createCompensation;if(compensation!==void 0){if(compensation!=="close"&&compensation!=="delete")throw new PmCliError("createCompensation must be close or delete.",EXIT_CODE.USAGE);controls.createCompensation=compensation}for(const key of["lockTtlSeconds","lockWaitMs"]){const raw=value[key];if(raw===void 0)continue;const parsed=typeof raw=="number"||typeof raw=="string"&&raw.trim().length>0?Number(raw):Number.NaN;if(!Number.isSafeInteger(parsed)||parsed<=0)throw new PmCliError(`${key} must be a positive safe integer.`,EXIT_CODE.USAGE);controls[key]=parsed}return controls}function isPlainObject(value){if(typeof value!="object"||value===null||Array.isArray(value))return!1;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function nearestKey(key,candidates){const limit=Math.max(1,Math.min(3,Math.floor(key.length/4)+1));let best;for(const candidate of candidates){const distance=levenshteinDistanceWithinLimit(key,candidate,limit);distance!==null&&(best===void 0||distance<best.distance)&&(best={key:candidate,distance})}return best?.key}function unknownKeyError(scope,key,candidates){const suggestion=nearestKey(key,candidates);return new PmCliError(suggestion===void 0?`${scope} does not recognize key "${key}". Allowed keys: ${candidates.join(", ")}.`:`${scope} does not recognize key "${key}". Did you mean "${suggestion}"?`,EXIT_CODE.USAGE,{code:"stdin_json_unknown_key",required:`Use documented ${scope} keys only.`,why:"Strict structured input prevents silent typos from creating incomplete or unintended mutations.",nextSteps:[suggestion===void 0?`Remove or correct "${key}".`:`Replace "${key}" with "${suggestion}".`]})}function parseJsonValue(input,label){try{return JSON.parse(input)}catch(error){const detail=error instanceof Error?error.message:String(error);throw new PmCliError(`${label} must be valid JSON: ${detail}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:`Pipe one valid JSON ${label.toLowerCase()} into stdin.`,why:"Structured mutation input must parse deterministically before any tracker write begins."})}}function validateMutationObject(value,index){if(!isPlainObject(value))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);for(const key of Object.keys(value))if(!MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,MUTATION_ROW_KEYS);return value}function validateMutationOptions(options,op,index,runtimeFields){if(options===void 0)return;if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} options must be an object.`,EXIT_CODE.USAGE);const allowed=[...MUTATION_OPTION_KEYS[op],...runtimeFieldOptionKeys(op,runtimeFields)];for(const key of Object.keys(options))if(!allowed.includes(key))throw unknownKeyError(`mutation ${index+1} ${op} options`,key,allowed);return assertMutationOptionValueTypes(options,op,index),options}function validateMutationKeyGuard(value,op,index,field){if(value===void 0)return;const allowedOp=field==="unique"?"create":"update";if(op!==allowedOp)throw new PmCliError(`Mutation ${index+1} ${field} is only valid for ${allowedOp} operations.`,EXIT_CODE.USAGE);const key=isPlainObject(value)&&typeof value.key=="string"?value.key.trim():"";if(!isPlainObject(value)||key.length===0||value.value===void 0||value.value===null)throw new PmCliError(`Mutation ${index+1} ${field} must be an object with a non-empty key and a value.`,EXIT_CODE.USAGE);return{key,value:value.value}}function validateMutationRow(value,index,runtimeFields){const mutation=validateMutationObject(value,index),{op,id,reason}=mutation;if(op!=="create"&&op!=="update"&&op!=="close"&&op!=="release")throw new PmCliError(`Mutation ${index+1} op must be create, update, close, or release.`,EXIT_CODE.USAGE);const options=validateMutationOptions(mutation.options,op,index,runtimeFields),unique=validateMutationKeyGuard(mutation.unique,op,index,"unique"),match=validateMutationKeyGuard(mutation.match,op,index,"match");if(typeof id!="string"||id.trim().length===0)throw new PmCliError(`Mutation ${index+1} requires a non-empty id.`,EXIT_CODE.USAGE);const normalizedId=id.trim();if(op==="close"){if(typeof reason!="string"||reason.trim().length===0)throw new PmCliError(`Mutation ${index+1} close requires a non-empty reason.`,EXIT_CODE.USAGE);return{op,id:normalizedId,reason:reason.trim(),...options===void 0?{}:{options}}}if(op==="release")return{op,id:normalizedId,...options===void 0?{}:{options}};if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} ${op} requires an options object.`,EXIT_CODE.USAGE);return{op,id:normalizedId,options,...unique===void 0?{}:{unique},...match===void 0?{}:{match}}}function parseItemMutationBatch(input,parseOptions={}){const parsed=parseJsonValue(input,"Mutation batch"),rows=Array.isArray(parsed)?parsed:isPlainObject(parsed)&&Array.isArray(parsed.mutations)?parsed.mutations:void 0;if(rows===void 0||rows.length===0)throw new PmCliError("Mutation batch must be a non-empty JSON array or an object with a non-empty mutations array.",EXIT_CODE.USAGE);const runtimeFields=parseOptions.runtimeFields??[];return rows.map((row,index)=>{const mutation=validateMutationRow(row,index,runtimeFields);return mutation.options!==void 0&&preserveMutationStdinTokenLiterals(mutation.options),mutation})}function deriveReferencedItemId(transactionId,reference,idPrefix){const digest=crypto.createHash("sha256").update(transactionId).update("\0").update(reference).digest("hex"),token=BigInt(`0x${digest.slice(0,16)}`).toString(36).padStart(13,"0").slice(-12);return`${normalizePrefix(idPrefix)}${token}`}function replaceExactMutationReference(value,references){if(typeof value!="string"||!value.startsWith("@"))return value;const reference=value.slice(1),resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "${value}".`,EXIT_CODE.USAGE);return resolved}function replaceDependencyReference(value,references){if(typeof value!="string")return value;const resolvedValue=value.replace(MUTATION_DEPENDENCY_REFERENCE_PATTERN,(_match,prefix,reference)=>{const resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "@${reference}".`,EXIT_CODE.USAGE);return`${prefix}id=${resolved}`});if(/(^|,)id=@/u.test(resolvedValue))throw new PmCliError(`Malformed mutation reference in dependency "${value}".`,EXIT_CODE.USAGE);return resolvedValue}function referencedAliases(value){return typeof value!="string"?[]:value.startsWith("@")?[value.slice(1)]:[...value.matchAll(MUTATION_DEPENDENCY_REFERENCE_PATTERN)].map(match=>match[1]).filter(reference=>reference!==void 0)}function assertAcyclicCreateReferences(rows,inputReferences){const graph=new Map;for(const[index,row]of rows.entries()){const reference=inputReferences[index];if(reference===void 0)continue;const mutationOptions=isPlainObject(row.options)?row.options:{},dependencies=Array.isArray(mutationOptions.dep)?mutationOptions.dep:mutationOptions.dep===void 0?[]:[mutationOptions.dep];graph.set(reference,[mutationOptions.parent,mutationOptions.blockedBy,...dependencies].flatMap(referencedAliases))}const visiting=new Set,visited=new Set,visit=reference=>{if(visiting.has(reference))throw new PmCliError(`Mutation reference cycle includes "@${reference}".`,EXIT_CODE.USAGE);if(!visited.has(reference)){visiting.add(reference);for(const dependency of graph.get(reference))graph.has(dependency)&&visit(dependency);visiting.delete(reference),visited.add(reference)}};for(const reference of graph.keys())visit(reference)}function parseJsonLinesRows(input){const candidates=input.split(/\r?\n/u).map((line,index)=>({line:line.trim(),number:index+1})).filter(entry=>entry.line.length>0);if(candidates.length<2)return null;const parsed=candidates.map(entry=>{try{return{...entry,ok:!0,value:JSON.parse(entry.line)}}catch(error){return{...entry,ok:!1,error}}}),first=parsed[0];if(!first.ok&&(first.line.startsWith("[")||first.line==="{"||parsed.at(-1).line==="}")||!parsed.some(entry=>entry.ok&&isPlainObject(entry.value)))return null;const rows=[];for(const entry of parsed){if(!entry.ok)throw new PmCliError(`Mutation document line ${entry.number} must be one valid JSON object: ${describeUnknownError(entry.error)}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:"Provide one JSON object per line (JSON Lines), a JSON array, or a {schema_version, mutations} document.",why:"Structured mutation input must parse deterministically before any tracker write begins."});if(!isPlainObject(entry.value))throw new PmCliError(`Mutation document line ${entry.number} must be a JSON object.`,EXIT_CODE.USAGE);rows.push(entry.value)}return rows}function isMutationEnvelope(value){return Object.hasOwn(value,"mutations")||Object.hasOwn(value,"schema_version")}function parseMutationDocumentEnvelope(input){let parsed;try{parsed=parseJsonValue(input,"Mutation document")}catch(error){const rows=parseJsonLinesRows(input);if(rows===null)throw error;return{schema_version:1,mutations:rows}}return Array.isArray(parsed)?{schema_version:1,mutations:parsed}:isPlainObject(parsed)&&!isMutationEnvelope(parsed)?{schema_version:1,mutations:[parsed]}:parsed}function normalizeDocumentRow(row,index){const normalized=Object.hasOwn(row,"op")?validateOperationRow(row,index):normalizeCreateDocumentRow(row,index);if(normalized.op==="create"&&normalized.id===void 0&&isPlainObject(normalized.item)&&normalized.item.id!==void 0){if(typeof normalized.item.id!="string")throw new PmCliError(`Mutation ${index+1} item id must be a string.`,EXIT_CODE.USAGE);normalized.id=normalized.item.id}return normalized}function validateOperationRow(row,index){for(const key of Object.keys(row))if(!DOCUMENT_MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,DOCUMENT_MUTATION_ROW_KEYS);if(row.item!==void 0&&row.op!=="create")throw new PmCliError(`Mutation ${index+1} item documents are only valid for create operations.`,EXIT_CODE.USAGE);if(row.item!==void 0&&row.options!==void 0)throw new PmCliError(`Mutation ${index+1} must declare either item or options, not both.`,EXIT_CODE.USAGE);return{...row}}function normalizeCreateDocumentRow(row,index){const{ref,id,item,...fields}=row,document=item!==void 0?item:fields;if(item!==void 0&&Object.keys(fields).length>0)throw new PmCliError(`Mutation ${index+1} must not mix item document keys with top-level fields.`,EXIT_CODE.USAGE);if(!isPlainObject(document))throw new PmCliError(`Mutation ${index+1} item document must be an object.`,EXIT_CODE.USAGE);return{op:"create",...ref===void 0?{}:{ref},...id===void 0?{}:{id},item:document}}function parseMutationDocumentRows(input){const envelope=parseMutationDocumentEnvelope(input);if(!isPlainObject(envelope))throw new PmCliError("Mutation document must be a JSON array or object.",EXIT_CODE.USAGE);for(const key of Object.keys(envelope))if(!MUTATION_DOCUMENT_KEYS.includes(key))throw unknownKeyError("mutation document",key,MUTATION_DOCUMENT_KEYS);if(envelope.schema_version!==void 0&&envelope.schema_version!==1)throw new PmCliError("Mutation document schema_version must be 1.",EXIT_CODE.USAGE);if(!Array.isArray(envelope.mutations)||envelope.mutations.length===0)throw new PmCliError("Mutation document requires a non-empty mutations array.",EXIT_CODE.USAGE);return envelope.mutations.map((row,index)=>{if(!isPlainObject(row))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);return normalizeDocumentRow(row,index)})}function excludeOwnCreateIds(upsert,rows,options){const ownIds=new Set(Object.values(collectMutationReferences(rows,options,Object.create(null))));for(const row of rows)row.op==="create"&&typeof row.id=="string"&&row.id.trim().length>0&&ownIds.add(normalizeItemId(row.id,options.idPrefix));return{...upsert,existing:upsert.existing.filter(candidate=>!ownIds.has(candidate.id))}}function synthesizeMissingReferences(rows){const declared=new Set(rows.map(row=>row.ref).filter(ref=>typeof ref=="string"));for(const[index,row]of rows.entries()){if(row.op!=="create"||row.ref!==void 0||typeof row.id=="string"&&row.id.trim().length>0)continue;const synthesized=`${SYNTHESIZED_REFERENCE_PREFIX}${index+1}`;if(declared.has(synthesized))throw new PmCliError(`Mutation ${index+1} receives the synthesized ref "${synthesized}", but another row declares that ref. Rename it or give mutation ${index+1} its own ref; ${SYNTHESIZED_REFERENCE_PREFIX}<n> aliases are reserved for rows without one.`,EXIT_CODE.USAGE);row.ref=synthesized}}function itemDocumentToBatchOptions(item,mode,runtimeFields){const converted=itemDocumentToMutationOptions(JSON.stringify(item),mode,{},runtimeFields);return delete converted.stdinJson,delete converted.documentAnnotationSnapshot,converted}function runtimeFieldOptionKeys(op,runtimeFields){return op!=="create"&&op!=="update"?[]:runtimeFields.filter(definition=>definition.commands.includes(op)).map(definition=>runtimeFieldOptionTarget(definition))}function convertItemDocumentRows(rows,runtimeFields){for(const row of rows)isPlainObject(row.item)&&(row.options=itemDocumentToBatchOptions(row.item,"create",runtimeFields),delete row.item)}function itemDocumentMetadataKey(key,runtimeFields){return runtimeFields.some(field=>field.metadata_key===key)?key:resolveCoreUpsertMetadataKey(key)}function readUpsertKeyValue(row,key,runtimeFields){const camel=key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase());if(isPlainObject(row.item)){if(row.item[key]!==void 0)return row.item[key];if(itemDocumentMetadataKey(camel,runtimeFields)===key&&row.item[camel]!==void 0)return row.item[camel]}const options=isPlainObject(row.options)?row.options:{},declared=runtimeFields.find(field=>field.metadata_key===key);return declared!==void 0&&options[runtimeFieldOptionTarget(declared)]!==void 0?options[runtimeFieldOptionTarget(declared)]:options[key]!==void 0?options[key]:options[camel]!==void 0?options[camel]:(Array.isArray(options.field)?options.field:[]).map(String).find(entry=>entry.startsWith(`${key}=`))?.slice(key.length+1)}function compareCodeUnits(left,right){return left<right?-1:+(left!==right)}function canonicalizeUpsertValue(value){return Array.isArray(value)?value.map(canonicalizeUpsertValue):isPlainObject(value)?Object.fromEntries(Object.entries(value).sort(([left],[right])=>compareCodeUnits(left,right)).map(([key,nested])=>[key,canonicalizeUpsertValue(nested)])):value}function stableUpsertValue(value){return typeof value=="string"?value.trim():JSON.stringify(canonicalizeUpsertValue(value))}function applyKeyedUpsert(rows,upsert,idPrefix,runtimeFields,references){const key=upsert.key.trim();if(key.length===0)throw new PmCliError("Upsert key must be a non-empty field name.",EXIT_CODE.USAGE);const existingByValue=indexUpsertCandidates(upsert.existing),seen=new Map,upserted=new Set;for(const[index,row]of rows.entries()){if(row.op!=="create")continue;const stable=readStableUpsertKey(row,key,index,seen,runtimeFields),match=resolveUpsertMatch(existingByValue.get(stable)??[],key,stable);if(match===void 0){row.unique={key,value:readUpsertKeyValue(row,key,runtimeFields)};continue}const id=normalizeItemId(match,idPrefix);typeof row.ref=="string"&&(references[row.ref]=id),convertRowToUpsertUpdate(row,id,runtimeFields,{key,value:readUpsertKeyValue(row,key,runtimeFields)}),upserted.add(index)}return upserted}function indexUpsertCandidates(existing){const existingByValue=new Map;for(const candidate of existing){if(candidate.value===void 0||candidate.value===null)continue;const stable=stableUpsertValue(candidate.value);existingByValue.set(stable,[...existingByValue.get(stable)??[],candidate.id])}return existingByValue}function readStableUpsertKey(row,key,index,seen,runtimeFields){const value=readUpsertKeyValue(row,key,runtimeFields);if(value==null||value==="")throw new PmCliError(`Mutation ${index+1} is missing the upsert key "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_missing",required:`Every create record must carry a non-empty "${key}" value when --upsert-key ${key} is set.`,why:"Keyed upsert identifies existing items by that field; a record without it cannot be matched or safely created."});const stable=stableUpsertValue(value),previous=seen.get(stable);if(previous!==void 0)throw new PmCliError(`Mutations ${previous+1} and ${index+1} share the upsert key value ${JSON.stringify(stable)} for "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_duplicate",required:"Each record must carry a distinct upsert key value within one batch.",why:"Two records with the same key would race for the same existing item."});return seen.set(stable,index),stable}function resolveUpsertMatch(matches,key,stable){if(matches.length>1)throw new PmCliError(`Upsert key "${key}" value ${JSON.stringify(stable)} matches ${matches.length} existing items: ${matches.join(", ")}.`,EXIT_CODE.CONFLICT,{code:"upsert_key_ambiguous",required:"Resolve the duplicate existing items or choose a key that is unique per item.",why:"An ambiguous key cannot select one item to update without guessing."});return matches[0]}function convertRowToUpsertUpdate(row,id,runtimeFields,match){const updateKeys=new Set([...MUTATION_OPTION_KEYS.update,...runtimeFieldOptionKeys("update",runtimeFields)]),options=isPlainObject(row.item)?itemDocumentToBatchOptions(row.item,"update",runtimeFields):Object.fromEntries(Object.entries(row.options).filter(([optionKey])=>updateKeys.has(optionKey)));delete row.item,delete row.ref,row.op="update",row.id=id,row.options=options,row.match=match}function collectMutationReferences(rows,options,references){for(const[index,row]of rows.entries())if(row.ref!==void 0){if(row.op!=="create")throw new PmCliError(`Mutation ${index+1} ref is only valid for create operations.`,EXIT_CODE.USAGE);if(typeof row.ref!="string"||!MUTATION_REFERENCE_PATTERN.test(row.ref))throw new PmCliError(`Mutation ${index+1} ref must match ${MUTATION_REFERENCE_PATTERN.source}.`,EXIT_CODE.USAGE);if(references[row.ref]!==void 0)throw new PmCliError(`Duplicate mutation ref "${row.ref}".`,EXIT_CODE.USAGE);references[row.ref]=typeof row.id=="string"&&row.id.trim().length>0?normalizeItemId(row.id,options.idPrefix):deriveReferencedItemId(options.transactionId,row.ref,options.idPrefix)}return references}function resolveItemMutationDocument(input,options){const rows=parseMutationDocumentRows(input);synthesizeMissingReferences(rows);const inputReferences=rows.map(row=>typeof row.ref=="string"?row.ref:void 0),references=Object.create(null),upserted=options.upsert===void 0?new Set:applyKeyedUpsert(rows,excludeOwnCreateIds(options.upsert,rows,options),options.idPrefix,options.runtimeFields??[],references);convertItemDocumentRows(rows,options.runtimeFields??[]),collectMutationReferences(rows,options,references),assertAcyclicCreateReferences(rows,inputReferences);const resolvedRows=rows.map(row=>{const resolved={...row},reference=typeof resolved.ref=="string"?resolved.ref:void 0;if(delete resolved.ref,reference!==void 0?resolved.id=references[reference]:resolved.id=replaceExactMutationReference(resolved.id,references),isPlainObject(resolved.options)){const mutationOptions={...resolved.options};for(const key of["parent","blockedBy"])mutationOptions[key]!==void 0&&(mutationOptions[key]=replaceExactMutationReference(mutationOptions[key],references));Array.isArray(mutationOptions.dep)?mutationOptions.dep=mutationOptions.dep.map(entry=>replaceDependencyReference(entry,references)):mutationOptions.dep!==void 0&&(mutationOptions.dep=replaceDependencyReference(mutationOptions.dep,references)),resolved.options=mutationOptions}return resolved}),mutations=parseItemMutationBatch(JSON.stringify(resolvedRows),{runtimeFields:options.runtimeFields??[]}),createIds=mutations.filter(mutation=>mutation.op==="create").map(mutation=>mutation.id);if(new Set(createIds).size!==createIds.length)throw new PmCliError("Duplicate resolved create id in mutation document.",EXIT_CODE.USAGE);const receiptRows=mutations.map((mutation,index)=>{const reference=inputReferences[index];return{index,...typeof reference=="string"?{ref:reference}:{},id:mutation.id,op:mutation.op,...upserted.has(index)?{upserted:!0}:{}}});return{schema_version:1,mutations,references,rows:receiptRows}}function serializePairs(value,keys){return keys.filter(key=>value[key]!==void 0&&value[key]!==null).map(key=>{const rendered=String(value[key]);return`${key}=${/[",\n\r\\]/u.test(rendered)?JSON.stringify(rendered):rendered}`}).join(",")}function serializeObjectArray(value,keys,label){if(value!==void 0){if(!Array.isArray(value)||!value.every(isPlainObject))throw new PmCliError(`${label} must be an array of objects.`,EXIT_CODE.USAGE);return value.map(entry=>serializePairs(entry,keys))}}function scalarItemOption(key,value){return key==="tags"&&Array.isArray(value)&&value.every(entry=>typeof entry=="string")?JSON.stringify(value):typeof value=="number"||typeof value=="boolean"?String(value):value}function appendFacetOptions(target,source,skipPersistedAnnotations=!1){for(const[sourceKey,targetKey,keys]of ITEM_MUTATION_FACET_OPTION_MAPPINGS){const sourceValue=source[sourceKey],serialized=serializeObjectArray(skipPersistedAnnotations&&["comments","notes","learnings"].includes(sourceKey)&&Array.isArray(sourceValue)?sourceValue.filter(entry=>!isPlainObject(entry)||entry.created_at===void 0):sourceValue,keys,sourceKey);serialized!==void 0&&serialized.length>0&&(target[targetKey]=serialized)}}function validateItemDocumentKeys(parsed,envelope){for(const key of Object.keys(parsed)){if(envelope){if(!ITEM_ENVELOPE_KEYS.includes(key))throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS);continue}if(!ITEM_FIELD_KEYS.has(key)&&nearestKey(key,[...ITEM_FIELD_KEYS])===void 0&&nearestKey(key,ITEM_ENVELOPE_KEYS)==="item")throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS)}}function resolveItemDocument(parsed){const envelope=Object.hasOwn(parsed,"item");validateItemDocumentKeys(parsed,envelope);const item=envelope?parsed.item:parsed;if(!isPlainObject(item))throw new PmCliError("Item document item must be an object.",EXIT_CODE.USAGE);return{item,...envelope&&isPlainObject(parsed.linked)?{linked:parsed.linked}:{}}}function appendItemDocumentEntry(documentOptions,key,value,mode,runtimeFields,explicitOptions){if((mode==="update"?UPDATE_READ_ONLY_ITEM_KEYS:READ_ONLY_ITEM_KEYS).has(key))return;const definition=runtimeFields.find(field=>field.metadata_key===key&&field.commands.includes(mode));if(definition!==void 0){const explicitValue=readRuntimeFieldOptionValue(explicitOptions,definition);documentOptions[runtimeFieldOptionTarget(definition)]=explicitValue!==void 0?explicitValue:definition.type==="array"||definition.type==="object"?JSON.stringify(value):value;return}if(!ITEM_FIELD_KEYS.has(key)){if(nearestKey(key,[...ITEM_FIELD_KEYS])!==void 0)throw unknownKeyError("item document",key,[...ITEM_FIELD_KEYS]);const fields=Array.isArray(documentOptions.field)?documentOptions.field:[];fields.push(`${key}=${typeof value=="string"?value:JSON.stringify(value)}`),documentOptions.field=fields;return}["dependencies","comments","notes","learnings","files","tests","docs","reminders","events","type_options"].includes(key)||(documentOptions[key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase())]=scalarItemOption(key,value))}function itemDocumentToMutationOptions(input,mode,explicitOptions={},runtimeFields=[]){const parsed=parseJsonValue(input,"Item document");if(!isPlainObject(parsed))throw new PmCliError("Item document must be a JSON object.",EXIT_CODE.USAGE);const{item,linked}=resolveItemDocument(parsed),documentOptions={};for(const[key,value]of Object.entries(item)){const metadataKey=itemDocumentMetadataKey(key,runtimeFields);metadataKey!==key&&Object.hasOwn(item,metadataKey)||appendItemDocumentEntry(documentOptions,metadataKey,value,mode,runtimeFields,explicitOptions)}if(appendFacetOptions(documentOptions,item,mode==="update"),linked!==void 0&&appendFacetOptions(documentOptions,linked),mode==="update"){const annotations=Object.fromEntries(["comments","notes","learnings"].filter(key=>Object.hasOwn(item,key)).map(key=>[key,item[key].filter(entry=>isPlainObject(entry)&&entry.created_at!==void 0)]));Object.keys(annotations).length>0&&(documentOptions.documentAnnotationSnapshot=annotations)}return{...documentOptions,...Object.fromEntries(Object.entries(explicitOptions).filter(([,value])=>value!==void 0)),...Array.isArray(documentOptions.field)&&Array.isArray(explicitOptions.field)?{field:[...documentOptions.field,...explicitOptions.field]}:{},stdinJson:void 0}}function validateItemMutationRows(value){return parseItemMutationBatch(JSON.stringify(value))}export{deriveMutationTransactionId,itemDocumentToMutationOptions,parseAtomicMutationControls,parseItemMutationBatch,resolveItemMutationDocument,stableUpsertValue,validateItemMutationRows};
1
+ (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9261d364-cb6c-5442-9ad0-41d0aefa8184")}catch{}})();import crypto from"node:crypto";import{ITEM_MUTATION_FACET_OPTION_MAPPINGS}from"./lifecycle/mutation-facet-contracts.js";import{normalizeItemId,normalizePrefix}from"../core/item/id.js";import{preserveMutationStdinTokenFields,preserveMutationStdinTokenLiterals}from"../core/item/parse.js";import{preserveLiteralMutationInputFields}from"../core/item/literal-mutation-input.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{levenshteinDistanceWithinLimit}from"../core/shared/levenshtein.js";import{describeUnknownError}from"./error-runtime.js";import{CLOSE_FLAG_CONTRACTS,RELEASE_FLAG_CONTRACTS}from"./cli-contracts/flag-contracts.js";import{CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS,UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS}from"./cli-contracts/commander-mutation-options.js";import{runtimeFieldOptionTarget}from"../core/schema/runtime-schema.js";import{CORE_ITEM_FIELD_KEYS as ITEM_FIELD_KEYS,resolveCoreUpsertMetadataKey}from"./lifecycle/core-item-fields.js";import{readRuntimeFieldOptionValue}from"../core/schema/runtime-field-values.js";import{assertMutationOptionValueTypes}from"./lifecycle/mutation-option-validation.js";const MUTATION_ROW_KEYS=["op","id","reason","options","unique","match"],REFERENCED_MUTATION_ROW_KEYS=[...MUTATION_ROW_KEYS,"ref"],DOCUMENT_MUTATION_ROW_KEYS=[...REFERENCED_MUTATION_ROW_KEYS,"item"],DERIVED_TRANSACTION_ID_PREFIX="mutate-",SYNTHESIZED_REFERENCE_PREFIX="row-",MUTATION_DOCUMENT_KEYS=["schema_version","mutations"],MUTATION_REFERENCE_SOURCE="[a-z][a-z0-9._-]{0,63}",MUTATION_REFERENCE_PATTERN=new RegExp(`^${MUTATION_REFERENCE_SOURCE}$`,"u"),MUTATION_DEPENDENCY_REFERENCE_PATTERN=new RegExp(`(^|,)id=@(${MUTATION_REFERENCE_SOURCE})(?=,|$)`,"gu"),flagToOptionKey=flag=>flag.slice(2).replaceAll(/-([a-z])/gu,(_match,letter)=>letter.toUpperCase()),ITEM_ENVELOPE_KEYS=["item","linked","schedule","claim_state","children","omission_receipt","row_contract","context_intent"],READ_ONLY_ITEM_KEYS=new Set(["created_at","updated_at","closed_at","version","format_version","path","author","collection_counts","notes_count","tests_count"]),UPDATE_READ_ONLY_ITEM_KEYS=new Set([...READ_ONLY_ITEM_KEYS,"completed_at","id"]),MUTATION_OPTION_KEYS={create:CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),update:UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),close:CLOSE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag)).filter(key=>key!=="reason"),release:RELEASE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag))};function deriveMutationTransactionId(input){const digest=crypto.createHash("sha256").update(input).digest("hex");return`${DERIVED_TRANSACTION_ID_PREFIX}${digest.slice(0,24)}`}function parseAtomicMutationControls(value){const controls={},compensation=value.createCompensation;if(compensation!==void 0){if(compensation!=="close"&&compensation!=="delete")throw new PmCliError("createCompensation must be close or delete.",EXIT_CODE.USAGE);controls.createCompensation=compensation}for(const key of["lockTtlSeconds","lockWaitMs"]){const raw=value[key];if(raw===void 0)continue;const parsed=typeof raw=="number"||typeof raw=="string"&&raw.trim().length>0?Number(raw):Number.NaN;if(!Number.isSafeInteger(parsed)||parsed<=0)throw new PmCliError(`${key} must be a positive safe integer.`,EXIT_CODE.USAGE);controls[key]=parsed}return controls}function isPlainObject(value){if(typeof value!="object"||value===null||Array.isArray(value))return!1;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function nearestKey(key,candidates){const limit=Math.max(1,Math.min(3,Math.floor(key.length/4)+1));let best;for(const candidate of candidates){const distance=levenshteinDistanceWithinLimit(key,candidate,limit);distance!==null&&(best===void 0||distance<best.distance)&&(best={key:candidate,distance})}return best?.key}function unknownKeyError(scope,key,candidates){const suggestion=nearestKey(key,candidates);return new PmCliError(suggestion===void 0?`${scope} does not recognize key "${key}". Allowed keys: ${candidates.join(", ")}.`:`${scope} does not recognize key "${key}". Did you mean "${suggestion}"?`,EXIT_CODE.USAGE,{code:"stdin_json_unknown_key",required:`Use documented ${scope} keys only.`,why:"Strict structured input prevents silent typos from creating incomplete or unintended mutations.",nextSteps:[suggestion===void 0?`Remove or correct "${key}".`:`Replace "${key}" with "${suggestion}".`]})}function parseJsonValue(input,label){try{return JSON.parse(input)}catch(error){const detail=error instanceof Error?error.message:String(error);throw new PmCliError(`${label} must be valid JSON: ${detail}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:`Pipe one valid JSON ${label.toLowerCase()} into stdin.`,why:"Structured mutation input must parse deterministically before any tracker write begins."})}}function validateMutationObject(value,index){if(!isPlainObject(value))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);for(const key of Object.keys(value))if(!MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,MUTATION_ROW_KEYS);return value}function validateMutationOptions(options,op,index,runtimeFields){if(options===void 0)return;if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} options must be an object.`,EXIT_CODE.USAGE);const allowed=[...MUTATION_OPTION_KEYS[op],...runtimeFieldOptionKeys(op,runtimeFields)];for(const key of Object.keys(options))if(!allowed.includes(key))throw unknownKeyError(`mutation ${index+1} ${op} options`,key,allowed);return assertMutationOptionValueTypes(options,op,index),options}function validateMutationKeyGuard(value,op,index,field){if(value===void 0)return;const allowedOp=field==="unique"?"create":"update";if(op!==allowedOp)throw new PmCliError(`Mutation ${index+1} ${field} is only valid for ${allowedOp} operations.`,EXIT_CODE.USAGE);const key=isPlainObject(value)&&typeof value.key=="string"?value.key.trim():"";if(!isPlainObject(value)||key.length===0||value.value===void 0||value.value===null)throw new PmCliError(`Mutation ${index+1} ${field} must be an object with a non-empty key and a value.`,EXIT_CODE.USAGE);return{key,value:value.value}}function validateMutationRow(value,index,runtimeFields){const mutation=validateMutationObject(value,index),{op,id,reason}=mutation;if(op!=="create"&&op!=="update"&&op!=="close"&&op!=="release")throw new PmCliError(`Mutation ${index+1} op must be create, update, close, or release.`,EXIT_CODE.USAGE);const options=validateMutationOptions(mutation.options,op,index,runtimeFields),unique=validateMutationKeyGuard(mutation.unique,op,index,"unique"),match=validateMutationKeyGuard(mutation.match,op,index,"match");if(typeof id!="string"||id.trim().length===0)throw new PmCliError(`Mutation ${index+1} requires a non-empty id.`,EXIT_CODE.USAGE);const normalizedId=id.trim();if(op==="close"){if(typeof reason!="string"||reason.trim().length===0)throw new PmCliError(`Mutation ${index+1} close requires a non-empty reason.`,EXIT_CODE.USAGE);return{op,id:normalizedId,reason:reason.trim(),...options===void 0?{}:{options}}}if(op==="release")return{op,id:normalizedId,...options===void 0?{}:{options}};if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} ${op} requires an options object.`,EXIT_CODE.USAGE);return{op,id:normalizedId,options,...unique===void 0?{}:{unique},...match===void 0?{}:{match}}}function parseItemMutationBatch(input,parseOptions={}){const parsed=parseJsonValue(input,"Mutation batch"),rows=Array.isArray(parsed)?parsed:isPlainObject(parsed)&&Array.isArray(parsed.mutations)?parsed.mutations:void 0;if(rows===void 0||rows.length===0)throw new PmCliError("Mutation batch must be a non-empty JSON array or an object with a non-empty mutations array.",EXIT_CODE.USAGE);const runtimeFields=parseOptions.runtimeFields??[];return rows.map((row,index)=>{const mutation=validateMutationRow(row,index,runtimeFields);return mutation.options!==void 0&&preserveMutationStdinTokenLiterals(mutation.options),mutation})}function deriveReferencedItemId(transactionId,reference,idPrefix){const digest=crypto.createHash("sha256").update(transactionId).update("\0").update(reference).digest("hex"),token=BigInt(`0x${digest.slice(0,16)}`).toString(36).padStart(13,"0").slice(-12);return`${normalizePrefix(idPrefix)}${token}`}function replaceExactMutationReference(value,references){if(typeof value!="string"||!value.startsWith("@"))return value;const reference=value.slice(1),resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "${value}".`,EXIT_CODE.USAGE);return resolved}function replaceDependencyReference(value,references){if(typeof value!="string")return value;const resolvedValue=value.replace(MUTATION_DEPENDENCY_REFERENCE_PATTERN,(_match,prefix,reference)=>{const resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "@${reference}".`,EXIT_CODE.USAGE);return`${prefix}id=${resolved}`});if(/(^|,)id=@/u.test(resolvedValue))throw new PmCliError(`Malformed mutation reference in dependency "${value}".`,EXIT_CODE.USAGE);return resolvedValue}function referencedAliases(value){return typeof value!="string"?[]:value.startsWith("@")?[value.slice(1)]:[...value.matchAll(MUTATION_DEPENDENCY_REFERENCE_PATTERN)].map(match=>match[1]).filter(reference=>reference!==void 0)}function assertAcyclicCreateReferences(rows,inputReferences){const graph=new Map;for(const[index,row]of rows.entries()){const reference=inputReferences[index];if(reference===void 0)continue;const mutationOptions=isPlainObject(row.options)?row.options:{},dependencies=Array.isArray(mutationOptions.dep)?mutationOptions.dep:mutationOptions.dep===void 0?[]:[mutationOptions.dep];graph.set(reference,[mutationOptions.parent,mutationOptions.blockedBy,...dependencies].flatMap(referencedAliases))}const visiting=new Set,visited=new Set,visit=reference=>{if(visiting.has(reference))throw new PmCliError(`Mutation reference cycle includes "@${reference}".`,EXIT_CODE.USAGE);if(!visited.has(reference)){visiting.add(reference);for(const dependency of graph.get(reference))graph.has(dependency)&&visit(dependency);visiting.delete(reference),visited.add(reference)}};for(const reference of graph.keys())visit(reference)}function parseJsonLinesRows(input){const candidates=input.split(/\r?\n/u).map((line,index)=>({line:line.trim(),number:index+1})).filter(entry=>entry.line.length>0);if(candidates.length<2)return null;const parsed=candidates.map(entry=>{try{return{...entry,ok:!0,value:JSON.parse(entry.line)}}catch(error){return{...entry,ok:!1,error}}}),first=parsed[0];if(!first.ok&&(first.line.startsWith("[")||first.line==="{"||parsed.at(-1).line==="}")||!parsed.some(entry=>entry.ok&&isPlainObject(entry.value)))return null;const rows=[];for(const entry of parsed){if(!entry.ok)throw new PmCliError(`Mutation document line ${entry.number} must be one valid JSON object: ${describeUnknownError(entry.error)}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:"Provide one JSON object per line (JSON Lines), a JSON array, or a {schema_version, mutations} document.",why:"Structured mutation input must parse deterministically before any tracker write begins."});if(!isPlainObject(entry.value))throw new PmCliError(`Mutation document line ${entry.number} must be a JSON object.`,EXIT_CODE.USAGE);rows.push(entry.value)}return rows}function isMutationEnvelope(value){return Object.hasOwn(value,"mutations")||Object.hasOwn(value,"schema_version")}function parseMutationDocumentEnvelope(input){let parsed;try{parsed=parseJsonValue(input,"Mutation document")}catch(error){const rows=parseJsonLinesRows(input);if(rows===null)throw error;return{schema_version:1,mutations:rows}}return Array.isArray(parsed)?{schema_version:1,mutations:parsed}:isPlainObject(parsed)&&!isMutationEnvelope(parsed)?{schema_version:1,mutations:[parsed]}:parsed}function normalizeDocumentRow(row,index){const normalized=Object.hasOwn(row,"op")?validateOperationRow(row,index):normalizeCreateDocumentRow(row,index);if(normalized.op==="create"&&normalized.id===void 0&&isPlainObject(normalized.item)&&normalized.item.id!==void 0){if(typeof normalized.item.id!="string")throw new PmCliError(`Mutation ${index+1} item id must be a string.`,EXIT_CODE.USAGE);normalized.id=normalized.item.id}return normalized}function validateOperationRow(row,index){for(const key of Object.keys(row))if(!DOCUMENT_MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,DOCUMENT_MUTATION_ROW_KEYS);if(row.item!==void 0&&row.op!=="create")throw new PmCliError(`Mutation ${index+1} item documents are only valid for create operations.`,EXIT_CODE.USAGE);if(row.item!==void 0&&row.options!==void 0)throw new PmCliError(`Mutation ${index+1} must declare either item or options, not both.`,EXIT_CODE.USAGE);return{...row}}function normalizeCreateDocumentRow(row,index){const{ref,id,item,...fields}=row,document=item!==void 0?item:fields;if(item!==void 0&&Object.keys(fields).length>0)throw new PmCliError(`Mutation ${index+1} must not mix item document keys with top-level fields.`,EXIT_CODE.USAGE);if(!isPlainObject(document))throw new PmCliError(`Mutation ${index+1} item document must be an object.`,EXIT_CODE.USAGE);return{op:"create",...ref===void 0?{}:{ref},...id===void 0?{}:{id},item:document}}function parseMutationDocumentRows(input){const envelope=parseMutationDocumentEnvelope(input);if(!isPlainObject(envelope))throw new PmCliError("Mutation document must be a JSON array or object.",EXIT_CODE.USAGE);for(const key of Object.keys(envelope))if(!MUTATION_DOCUMENT_KEYS.includes(key))throw unknownKeyError("mutation document",key,MUTATION_DOCUMENT_KEYS);if(envelope.schema_version!==void 0&&envelope.schema_version!==1)throw new PmCliError("Mutation document schema_version must be 1.",EXIT_CODE.USAGE);if(!Array.isArray(envelope.mutations)||envelope.mutations.length===0)throw new PmCliError("Mutation document requires a non-empty mutations array.",EXIT_CODE.USAGE);return envelope.mutations.map((row,index)=>{if(!isPlainObject(row))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);return normalizeDocumentRow(row,index)})}function excludeOwnCreateIds(upsert,rows,options){const ownIds=new Set(Object.values(collectMutationReferences(rows,options,Object.create(null))));for(const row of rows)row.op==="create"&&typeof row.id=="string"&&row.id.trim().length>0&&ownIds.add(normalizeItemId(row.id,options.idPrefix));return{...upsert,existing:upsert.existing.filter(candidate=>!ownIds.has(candidate.id))}}function synthesizeMissingReferences(rows){const declared=new Set(rows.map(row=>row.ref).filter(ref=>typeof ref=="string"));for(const[index,row]of rows.entries()){if(row.op!=="create"||row.ref!==void 0||typeof row.id=="string"&&row.id.trim().length>0)continue;const synthesized=`${SYNTHESIZED_REFERENCE_PREFIX}${index+1}`;if(declared.has(synthesized))throw new PmCliError(`Mutation ${index+1} receives the synthesized ref "${synthesized}", but another row declares that ref. Rename it or give mutation ${index+1} its own ref; ${SYNTHESIZED_REFERENCE_PREFIX}<n> aliases are reserved for rows without one.`,EXIT_CODE.USAGE);row.ref=synthesized}}function itemDocumentToBatchOptions(item,mode,runtimeFields){const converted=itemDocumentToMutationOptions(JSON.stringify(item),mode,{},runtimeFields);return delete converted.stdinJson,delete converted.documentAnnotationSnapshot,converted}function runtimeFieldOptionKeys(op,runtimeFields){return op!=="create"&&op!=="update"?[]:runtimeFields.filter(definition=>definition.commands.includes(op)).map(definition=>runtimeFieldOptionTarget(definition))}function convertItemDocumentRows(rows,runtimeFields){for(const row of rows)isPlainObject(row.item)&&(row.options=itemDocumentToBatchOptions(row.item,"create",runtimeFields),delete row.item)}function itemDocumentMetadataKey(key,runtimeFields){return runtimeFields.some(field=>field.metadata_key===key)?key:resolveCoreUpsertMetadataKey(key)}function readUpsertKeyValue(row,key,runtimeFields){const camel=key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase());if(isPlainObject(row.item)){if(row.item[key]!==void 0)return row.item[key];if(itemDocumentMetadataKey(camel,runtimeFields)===key&&row.item[camel]!==void 0)return row.item[camel]}const options=isPlainObject(row.options)?row.options:{},declared=runtimeFields.find(field=>field.metadata_key===key);return declared!==void 0&&options[runtimeFieldOptionTarget(declared)]!==void 0?options[runtimeFieldOptionTarget(declared)]:options[key]!==void 0?options[key]:options[camel]!==void 0?options[camel]:(Array.isArray(options.field)?options.field:[]).map(String).find(entry=>entry.startsWith(`${key}=`))?.slice(key.length+1)}function compareCodeUnits(left,right){return left<right?-1:+(left!==right)}function canonicalizeUpsertValue(value){return Array.isArray(value)?value.map(canonicalizeUpsertValue):isPlainObject(value)?Object.fromEntries(Object.entries(value).sort(([left],[right])=>compareCodeUnits(left,right)).map(([key,nested])=>[key,canonicalizeUpsertValue(nested)])):value}function stableUpsertValue(value){return typeof value=="string"?value.trim():JSON.stringify(canonicalizeUpsertValue(value))}function applyKeyedUpsert(rows,upsert,idPrefix,runtimeFields,references){const key=upsert.key.trim();if(key.length===0)throw new PmCliError("Upsert key must be a non-empty field name.",EXIT_CODE.USAGE);const existingByValue=indexUpsertCandidates(upsert.existing),seen=new Map,upserted=new Set;for(const[index,row]of rows.entries()){if(row.op!=="create")continue;const stable=readStableUpsertKey(row,key,index,seen,runtimeFields),match=resolveUpsertMatch(existingByValue.get(stable)??[],key,stable);if(match===void 0){row.unique={key,value:readUpsertKeyValue(row,key,runtimeFields)};continue}const id=normalizeItemId(match,idPrefix);typeof row.ref=="string"&&(references[row.ref]=id),convertRowToUpsertUpdate(row,id,runtimeFields,{key,value:readUpsertKeyValue(row,key,runtimeFields)}),upserted.add(index)}return upserted}function indexUpsertCandidates(existing){const existingByValue=new Map;for(const candidate of existing){if(candidate.value===void 0||candidate.value===null)continue;const stable=stableUpsertValue(candidate.value);existingByValue.set(stable,[...existingByValue.get(stable)??[],candidate.id])}return existingByValue}function readStableUpsertKey(row,key,index,seen,runtimeFields){const value=readUpsertKeyValue(row,key,runtimeFields);if(value==null||value==="")throw new PmCliError(`Mutation ${index+1} is missing the upsert key "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_missing",required:`Every create record must carry a non-empty "${key}" value when --upsert-key ${key} is set.`,why:"Keyed upsert identifies existing items by that field; a record without it cannot be matched or safely created."});const stable=stableUpsertValue(value),previous=seen.get(stable);if(previous!==void 0)throw new PmCliError(`Mutations ${previous+1} and ${index+1} share the upsert key value ${JSON.stringify(stable)} for "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_duplicate",required:"Each record must carry a distinct upsert key value within one batch.",why:"Two records with the same key would race for the same existing item."});return seen.set(stable,index),stable}function resolveUpsertMatch(matches,key,stable){if(matches.length>1)throw new PmCliError(`Upsert key "${key}" value ${JSON.stringify(stable)} matches ${matches.length} existing items: ${matches.join(", ")}.`,EXIT_CODE.CONFLICT,{code:"upsert_key_ambiguous",required:"Resolve the duplicate existing items or choose a key that is unique per item.",why:"An ambiguous key cannot select one item to update without guessing."});return matches[0]}function convertRowToUpsertUpdate(row,id,runtimeFields,match){const updateKeys=new Set([...MUTATION_OPTION_KEYS.update,...runtimeFieldOptionKeys("update",runtimeFields)]),options=isPlainObject(row.item)?itemDocumentToBatchOptions(row.item,"update",runtimeFields):Object.fromEntries(Object.entries(row.options).filter(([optionKey])=>updateKeys.has(optionKey)));delete row.item,delete row.ref,row.op="update",row.id=id,row.options=options,row.match=match}function collectMutationReferences(rows,options,references){for(const[index,row]of rows.entries())if(row.ref!==void 0){if(row.op!=="create")throw new PmCliError(`Mutation ${index+1} ref is only valid for create operations.`,EXIT_CODE.USAGE);if(typeof row.ref!="string"||!MUTATION_REFERENCE_PATTERN.test(row.ref))throw new PmCliError(`Mutation ${index+1} ref must match ${MUTATION_REFERENCE_PATTERN.source}.`,EXIT_CODE.USAGE);if(references[row.ref]!==void 0)throw new PmCliError(`Duplicate mutation ref "${row.ref}".`,EXIT_CODE.USAGE);references[row.ref]=typeof row.id=="string"&&row.id.trim().length>0?normalizeItemId(row.id,options.idPrefix):deriveReferencedItemId(options.transactionId,row.ref,options.idPrefix)}return references}function resolveItemMutationDocument(input,options){const rows=parseMutationDocumentRows(input);synthesizeMissingReferences(rows);const inputReferences=rows.map(row=>typeof row.ref=="string"?row.ref:void 0),references=Object.create(null),upserted=options.upsert===void 0?new Set:applyKeyedUpsert(rows,excludeOwnCreateIds(options.upsert,rows,options),options.idPrefix,options.runtimeFields??[],references);convertItemDocumentRows(rows,options.runtimeFields??[]),collectMutationReferences(rows,options,references),assertAcyclicCreateReferences(rows,inputReferences);const resolvedRows=rows.map(row=>{const resolved={...row},reference=typeof resolved.ref=="string"?resolved.ref:void 0;if(delete resolved.ref,reference!==void 0?resolved.id=references[reference]:resolved.id=replaceExactMutationReference(resolved.id,references),isPlainObject(resolved.options)){const mutationOptions={...resolved.options};for(const key of["parent","blockedBy"])mutationOptions[key]!==void 0&&(mutationOptions[key]=replaceExactMutationReference(mutationOptions[key],references));Array.isArray(mutationOptions.dep)?mutationOptions.dep=mutationOptions.dep.map(entry=>replaceDependencyReference(entry,references)):mutationOptions.dep!==void 0&&(mutationOptions.dep=replaceDependencyReference(mutationOptions.dep,references)),resolved.options=mutationOptions}return resolved}),mutations=parseItemMutationBatch(JSON.stringify(resolvedRows),{runtimeFields:options.runtimeFields??[]}),createIds=mutations.filter(mutation=>mutation.op==="create").map(mutation=>mutation.id);if(new Set(createIds).size!==createIds.length)throw new PmCliError("Duplicate resolved create id in mutation document.",EXIT_CODE.USAGE);const receiptRows=mutations.map((mutation,index)=>{const reference=inputReferences[index];return{index,...typeof reference=="string"?{ref:reference}:{},id:mutation.id,op:mutation.op,...upserted.has(index)?{upserted:!0}:{}}});return{schema_version:1,mutations,references,rows:receiptRows}}function serializePairs(value,keys){return keys.filter(key=>value[key]!==void 0&&value[key]!==null).map(key=>{const rendered=String(value[key]);return`${key}=${/[",\n\r\\]/u.test(rendered)?JSON.stringify(rendered):rendered}`}).join(",")}function serializeObjectArray(value,keys,label){if(value!==void 0){if(!Array.isArray(value)||!value.every(isPlainObject))throw new PmCliError(`${label} must be an array of objects.`,EXIT_CODE.USAGE);return value.map(entry=>serializePairs(entry,keys))}}function scalarItemOption(key,value){return key==="tags"&&Array.isArray(value)&&value.every(entry=>typeof entry=="string")?JSON.stringify(value):typeof value=="number"||typeof value=="boolean"?String(value):value}function appendFacetOptions(target,source,skipPersistedAnnotations=!1){for(const[sourceKey,targetKey,keys]of ITEM_MUTATION_FACET_OPTION_MAPPINGS){const sourceValue=source[sourceKey],serialized=serializeObjectArray(skipPersistedAnnotations&&["comments","notes","learnings"].includes(sourceKey)&&Array.isArray(sourceValue)?sourceValue.filter(entry=>!isPlainObject(entry)||entry.created_at===void 0):sourceValue,keys,sourceKey);serialized!==void 0&&serialized.length>0&&(target[targetKey]=serialized)}}function validateItemDocumentKeys(parsed,envelope){for(const key of Object.keys(parsed)){if(envelope){if(!ITEM_ENVELOPE_KEYS.includes(key))throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS);continue}if(!ITEM_FIELD_KEYS.has(key)&&nearestKey(key,[...ITEM_FIELD_KEYS])===void 0&&nearestKey(key,ITEM_ENVELOPE_KEYS)==="item")throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS)}}function resolveItemDocument(parsed){const envelope=Object.hasOwn(parsed,"item");validateItemDocumentKeys(parsed,envelope);const item=envelope?parsed.item:parsed;if(!isPlainObject(item))throw new PmCliError("Item document item must be an object.",EXIT_CODE.USAGE);return{item,...envelope&&isPlainObject(parsed.linked)?{linked:parsed.linked}:{}}}function appendItemDocumentEntry(documentOptions,key,value,mode,runtimeFields,explicitOptions){if((mode==="update"?UPDATE_READ_ONLY_ITEM_KEYS:READ_ONLY_ITEM_KEYS).has(key))return;const definition=runtimeFields.find(field=>field.metadata_key===key&&field.commands.includes(mode));if(definition!==void 0){const explicitValue=readRuntimeFieldOptionValue(explicitOptions,definition);documentOptions[runtimeFieldOptionTarget(definition)]=explicitValue!==void 0?explicitValue:definition.type==="array"||definition.type==="object"?JSON.stringify(value):value;return}if(!ITEM_FIELD_KEYS.has(key)){if(nearestKey(key,[...ITEM_FIELD_KEYS])!==void 0)throw unknownKeyError("item document",key,[...ITEM_FIELD_KEYS]);const fields=Array.isArray(documentOptions.field)?documentOptions.field:[];fields.push(`${key}=${typeof value=="string"?value:JSON.stringify(value)}`),documentOptions.field=fields;return}["dependencies","comments","notes","learnings","files","tests","docs","reminders","events","type_options"].includes(key)||(documentOptions[key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase())]=scalarItemOption(key,value))}function explicitDocumentOptionKeys(explicitOptions,mode,runtimeFields){const explicitKeys=new Set(Object.entries(explicitOptions).filter(([,value])=>value!==void 0).map(([key])=>key)),contracts=mode==="create"?CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS:UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS;for(const{target,keys}of contracts)keys.some(key=>explicitKeys.has(key))&&explicitKeys.add(target);for(const definition of runtimeFields)readRuntimeFieldOptionValue(explicitOptions,definition)!==void 0&&explicitKeys.add(runtimeFieldOptionTarget(definition));return explicitKeys}function removeOverriddenDocumentOptions(documentOptions,mode,explicitKeys){const contracts=mode==="create"?CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS:UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS;for(const{target}of contracts)target!=="field"&&explicitKeys.has(target)&&delete documentOptions[target]}function itemDocumentToMutationOptions(input,mode,explicitOptions={},runtimeFields=[]){const parsed=parseJsonValue(input,"Item document");if(!isPlainObject(parsed))throw new PmCliError("Item document must be a JSON object.",EXIT_CODE.USAGE);const{item,linked}=resolveItemDocument(parsed),documentOptions={};for(const[key,value]of Object.entries(item)){const metadataKey=itemDocumentMetadataKey(key,runtimeFields);metadataKey!==key&&Object.hasOwn(item,metadataKey)||appendItemDocumentEntry(documentOptions,metadataKey,value,mode,runtimeFields,explicitOptions)}appendFacetOptions(documentOptions,item,mode==="update"),linked!==void 0&&appendFacetOptions(documentOptions,linked);const explicitKeys=explicitDocumentOptionKeys(explicitOptions,mode,runtimeFields);if(removeOverriddenDocumentOptions(documentOptions,mode,explicitKeys),mode==="update"){const annotations=Object.fromEntries(["comments","notes","learnings"].filter(key=>Object.hasOwn(item,key)&&!explicitKeys.has(key.slice(0,-1))).map(key=>[key,item[key].filter(entry=>isPlainObject(entry)&&entry.created_at!==void 0)]));Object.keys(annotations).length>0&&(documentOptions.documentAnnotationSnapshot=annotations)}const merged={...documentOptions,...Object.fromEntries(Object.entries(explicitOptions).filter(([,value])=>value!==void 0)),...Array.isArray(documentOptions.field)&&Array.isArray(explicitOptions.field)?{field:[...documentOptions.field,...explicitOptions.field]}:{},stdinJson:void 0},literalKeys=Object.keys(documentOptions).filter(key=>!explicitKeys.has(key));return preserveMutationStdinTokenFields(preserveLiteralMutationInputFields(merged,literalKeys),literalKeys)}function validateItemMutationRows(value){return parseItemMutationBatch(JSON.stringify(value))}export{deriveMutationTransactionId,itemDocumentToMutationOptions,parseAtomicMutationControls,parseItemMutationBatch,resolveItemMutationDocument,stableUpsertValue,validateItemMutationRows};
2
2
  //# sourceMappingURL=structured-mutations.js.map
@@ -1,2 +1,2 @@
1
- (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9c0640f0-505c-5f4a-bb7a-daa3cc775239")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import fs from"node:fs/promises";import path from"node:path";const MILLISECONDS_PER_DAY=1440*60*1e3,TERMINAL_JOURNAL_STATUSES=new Set(["committed","compensated"]);function readJournalStatusAndTimestamp(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return{status:null,updatedAt:null}}if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))return{status:null,updatedAt:null};const record=parsed;return{status:typeof record.status=="string"?record.status:null,updatedAt:typeof record.updatedAt=="string"?record.updatedAt:null}}function classifyWorkspaceTransactionJournal(file,status,updatedAt,nowMs,retentionMs){const updatedAtMs=updatedAt===null?Number.NaN:Date.parse(updatedAt);if(status===null||!Number.isFinite(updatedAtMs))return{entry:{file,status,age_seconds:null,stale:!1,reason:"unparseable"},remove:!1};const ageMs=nowMs-updatedAtMs;return TERMINAL_JOURNAL_STATUSES.has(status)?ageMs>retentionMs?{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!0},remove:!0}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"fresh"},remove:!1}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"active"},remove:!1}}async function processWorkspaceTransactionJournal(absolutePath,relativePath,fileName,options){const warnings=options.hooks?.onRead?await options.hooks.onRead(absolutePath):[],journal=readJournalStatusAndTimestamp(await fs.readFile(absolutePath,"utf8")),decision=classifyWorkspaceTransactionJournal(relativePath,journal.status,journal.updatedAt,options.nowMs,options.retentionMs);return decision.entry.reason==="unparseable"&&warnings.push(`transaction_journal_unparseable:${fileName}`),decision.remove&&!options.dryRun&&(await fs.rm(absolutePath,{force:!0}),options.hooks?.onWrite&&warnings.push(...await options.hooks.onWrite(absolutePath))),{decision,warnings}}async function runWorkspaceTransactionGc(pmRoot,options){const{dryRun,hooks}=options,nowMs=options.now??Date.now(),retentionMs=Math.max(0,options.retentionDays)*MILLISECONDS_PER_DAY,journalsDir=path.join(pmRoot,"transactions","sdk"),result={scanned:0,removed:[],retained:[],warnings:[],entries:[]};let files;try{files=(await fs.readdir(journalsDir)).filter(file=>file.endsWith(".json")).sort((left,right)=>left.localeCompare(right))}catch(error){if(isFileMissingError(error))return result;throw error}for(const file of files){const absPath=path.join(journalsDir,file),relPath=`transactions/sdk/${file}`,fileResult=await processWorkspaceTransactionJournal(absPath,relPath,file,{dryRun,hooks,nowMs,retentionMs});result.warnings.push(...fileResult.warnings),result.scanned+=1;const{decision}=fileResult;if(!decision.remove){result.retained.push(relPath),result.entries.push(decision.entry);continue}result.removed.push(relPath),result.entries.push(decision.entry)}return result}export{runWorkspaceTransactionGc};
1
+ (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ff5e2370-3b70-5f71-9492-72906f642c85")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import fs from"node:fs/promises";import path from"node:path";const MILLISECONDS_PER_DAY=1440*60*1e3,TERMINAL_JOURNAL_STATUSES=new Set(["committed","compensated"]);function readJournalStatusAndTimestamp(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return{status:null,updatedAt:null}}if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))return{status:null,updatedAt:null};const record=parsed;return{status:record.abandonment!==void 0?"abandoned":typeof record.status=="string"?record.status:null,updatedAt:typeof record.updatedAt=="string"?record.updatedAt:null}}function classifyWorkspaceTransactionJournal(file,status,updatedAt,nowMs,retentionMs){const updatedAtMs=updatedAt===null?Number.NaN:Date.parse(updatedAt);if(status===null||!Number.isFinite(updatedAtMs))return{entry:{file,status,age_seconds:null,stale:!1,reason:"unparseable"},remove:!1};const ageMs=nowMs-updatedAtMs;return TERMINAL_JOURNAL_STATUSES.has(status)?ageMs>retentionMs?{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!0},remove:!0}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"fresh"},remove:!1}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"active"},remove:!1}}async function processWorkspaceTransactionJournal(absolutePath,relativePath,fileName,options){const warnings=options.hooks?.onRead?await options.hooks.onRead(absolutePath):[],journal=readJournalStatusAndTimestamp(await fs.readFile(absolutePath,"utf8")),decision=classifyWorkspaceTransactionJournal(relativePath,journal.status,journal.updatedAt,options.nowMs,options.retentionMs);return decision.entry.reason==="unparseable"&&warnings.push(`transaction_journal_unparseable:${fileName}`),decision.remove&&!options.dryRun&&(await fs.rm(absolutePath,{force:!0}),options.hooks?.onWrite&&warnings.push(...await options.hooks.onWrite(absolutePath))),{decision,warnings}}async function runWorkspaceTransactionGc(pmRoot,options){const{dryRun,hooks}=options,nowMs=options.now??Date.now(),retentionMs=Math.max(0,options.retentionDays)*MILLISECONDS_PER_DAY,journalsDir=path.join(pmRoot,"transactions","sdk"),result={scanned:0,removed:[],retained:[],warnings:[],entries:[]};let files;try{files=(await fs.readdir(journalsDir)).filter(file=>file.endsWith(".json")).sort((left,right)=>left.localeCompare(right))}catch(error){if(isFileMissingError(error))return result;throw error}for(const file of files){const absPath=path.join(journalsDir,file),relPath=`transactions/sdk/${file}`,fileResult=await processWorkspaceTransactionJournal(absPath,relPath,file,{dryRun,hooks,nowMs,retentionMs});result.warnings.push(...fileResult.warnings),result.scanned+=1;const{decision}=fileResult;if(!decision.remove){result.retained.push(relPath),result.entries.push(decision.entry);continue}result.removed.push(relPath),result.entries.push(decision.entry)}return result}export{runWorkspaceTransactionGc};
2
2
  //# sourceMappingURL=workspace-transaction-gc.js.map
@@ -49,6 +49,8 @@ transactionId:string;
49
49
  author:string;
50
50
  /** Ordered idempotent mutations; compensations run in reverse order. */
51
51
  steps:readonly WorkspaceTransactionStep[];
52
+ /** JSON plan retained for adapter-owned recovery; callers must not store secrets. */
53
+ recoveryData?:WorkspaceTransactionJsonValue;
52
54
  /** Lock lifetime in seconds; size this above the longest expected attempt. */
53
55
  lockTtlSeconds?:number;
54
56
  /** Maximum time to wait for the workspace writer lock. */
@@ -76,6 +78,43 @@ export declare class WorkspaceTransactionInterruptedError extends Error {
76
78
  /** Create one explicit crash-boundary interruption. */
77
79
  constructor(message?:string);
78
80
  }
81
+ /** Durable coordinator lifecycle, including explicitly retired identities. */
82
+ export type WorkspaceTransactionJournalStatus = "applying" | "compensating" | "compensated" | "committed" | "abandoned";
83
+ /** Durable coordinator receipt; step lists describe journaled progress, not a fresh domain inspection. */
84
+ export interface WorkspaceTransactionJournal {
85
+ /** Version 2 fences older coordinators once explicit retirement begins. */
86
+ schemaVersion:1 | 2;
87
+ /** Stable transaction identity. */
88
+ transactionId:string;
89
+ /** Actor who prepared the attempt. */
90
+ author:string;
91
+ /** Last durable lifecycle state. */
92
+ status:WorkspaceTransactionJournalStatus;
93
+ /** One-based attempt number. */
94
+ attempt:number;
95
+ /** Initial preparation timestamp. */
96
+ createdAt:string;
97
+ /** Latest journal write timestamp. */
98
+ updatedAt:string;
99
+ /** Ordered payload-bound step identifiers. */
100
+ stepIds:string[];
101
+ /** Steps durably admitted before forward execution. */
102
+ startedStepIds:string[];
103
+ /** Steps recorded as applied. */
104
+ completedStepIds:string[];
105
+ /** Recorded results for completed steps. */
106
+ results:Record<string,WorkspaceTransactionJsonValue>;
107
+ /** Captured pre-write recovery receipts. */
108
+ compensationData:Record<string,WorkspaceTransactionJsonValue>;
109
+ /** Adapter-owned original plan, absent in legacy journals. */
110
+ recoveryData?:WorkspaceTransactionJsonValue;
111
+ /** Explicit retirement rationale and optional replacement identity. */
112
+ abandonment?:{
113
+ reason:string;
114
+ author:string;
115
+ supersededBy?:string;
116
+ };
117
+ }
79
118
  /** Validate transaction identity before preview staging or acquiring any writer lock. */
80
119
  export declare function validateWorkspaceTransactionIdentity(options:Pick<CommitWorkspaceTransactionOptions,"pmRoot" | "transactionId" | "author">):Pick<CommitWorkspaceTransactionOptions,"pmRoot" | "transactionId" | "author">;
81
120
  /**
@@ -85,3 +124,18 @@ export declare function validateWorkspaceTransactionIdentity(options:Pick<Commit
85
124
  * replay without deleting or rewriting immutable domain histories.
86
125
  */
87
126
  export declare function commitWorkspaceTransaction(options:CommitWorkspaceTransactionOptions):Promise<WorkspaceTransactionCommitResult>;
127
+ /** Read an atomically published journal without changing tracker state; undefined means no retained receipt. */
128
+ export declare function readWorkspaceTransaction(pmRoot:string,transactionId:string):Promise<WorkspaceTransactionJournal | undefined>;
129
+ /** SHA-256 identity of the ordered, payload-bound step identifiers. */
130
+ export declare function workspaceTransactionPlanDigest(journal:Pick<WorkspaceTransactionJournal,"stepIds">):string;
131
+ /** Explicit recovery request; the adapter reconstructs compensation from the retained original plan under the coordinator lock. */
132
+ export interface AbandonWorkspaceTransactionOptions extends Omit<CommitWorkspaceTransactionOptions,"steps" | "recoveryData"> {
133
+ /** Required human-readable recovery rationale. */
134
+ reason:string;
135
+ /** Optional fresh identity chosen by the caller for its replacement plan; this does not reserve it. */
136
+ supersededBy?:string;
137
+ /** Rebuild original steps without applying forward mutations. */
138
+ resolveSteps(journal:WorkspaceTransactionJournal):readonly WorkspaceTransactionStep[];
139
+ }
140
+ /** Compensate interrupted owned effects and permanently retire the id; committed work is never undone. */
141
+ export declare function abandonWorkspaceTransaction(options:AbandonWorkspaceTransactionOptions):Promise<WorkspaceTransactionJournal>;
@@ -1,3 +1,3 @@
1
- (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d913251a-fc99-5767-b19d-ae9473adc138")}catch{}})();import{mkdir,readFile}from"node:fs/promises";import path from"node:path";import{acquireLock}from"../core/lock/lock.js";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{nowIso}from"../core/shared/time.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";class WorkspaceTransactionInterruptedError extends Error{constructor(message="Workspace transaction interrupted"){super(message),this.name="WorkspaceTransactionInterruptedError"}}const WORKSPACE_TRANSACTION_LOCK_ID="sdk-workspace-transaction",TRANSACTION_ID_PATTERN=/^[a-zA-Z0-9._-]+$/,DEFAULT_LOCK_TTL_SECONDS=30,DEFAULT_LOCK_WAIT_MS=3e3;function requiredIdentifier(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized||!TRANSACTION_ID_PATTERN.test(normalized))throw new TypeError(`${label} must match [a-zA-Z0-9._-]+`);return normalized}function requiredText(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized)throw new TypeError(`${label} must be non-empty`);return normalized}function validateWorkspaceTransactionIdentity(options){return{pmRoot:requiredText(options.pmRoot,"Transaction pmRoot"),transactionId:requiredIdentifier(options.transactionId,"Transaction id"),author:requiredText(options.author,"Transaction author")}}function positiveInteger(value,fallback,label){const resolved=value??fallback;if(!Number.isInteger(resolved)||resolved<=0)throw new TypeError(`${label} must be a positive integer`);return resolved}function journalPath(pmRoot,transactionId){return path.join(pmRoot,"transactions","sdk",`${transactionId}.json`)}async function writeJournal(pmRoot,journal){journal.updatedAt=nowIso();const target=journalPath(pmRoot,journal.transactionId);await mkdir(path.dirname(target),{recursive:!0}),await writeFileAtomic(target,`${JSON.stringify(journal,null,2)}
2
- `)}function hasValidJournalMetadata(journal){return Number.isInteger(journal.attempt)&&typeof journal.author=="string"&&typeof journal.createdAt=="string"&&typeof journal.updatedAt=="string"}function createResultMap(source){return Object.assign(Object.create(null),source)}function cloneJournalValue(value,label){try{const serialized=JSON.stringify(value,(_key,nestedValue)=>{if(typeof nestedValue=="number"&&!Number.isFinite(nestedValue))throw new TypeError(`${label} contains a non-finite number`);if(nestedValue===void 0||typeof nestedValue=="function"||typeof nestedValue=="symbol")throw new TypeError(`${label} contains a non-JSON value`);return nestedValue});return JSON.parse(serialized)}catch(error){throw new TypeError(`${label} must be JSON serializable`,{cause:error})}}function cloneOptionalJournalValue(value,label){return value===void 0?void 0:cloneJournalValue(value,label)}function setJournalEntry(target,stepId,value,label){const cloned=cloneOptionalJournalValue(value,label);cloned===void 0?delete target[stepId]:target[stepId]=cloned}function isJournalMap(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function hasUniqueKnownStepIds(value,stepIds){return Array.isArray(value)&&new Set(value).size===value.length&&value.every(stepId=>typeof stepId=="string"&&stepIds.includes(stepId))}function hasValidJournalLifecycle(journal,stepIds){if(!hasUniqueKnownStepIds(journal.startedStepIds,stepIds)||!hasUniqueKnownStepIds(journal.completedStepIds,stepIds)||!isJournalMap(journal.results)||!isJournalMap(journal.compensationData))return!1;const startedStepIds=journal.startedStepIds,started=new Set(startedStepIds),completedStepIds=journal.completedStepIds,completed=new Set(completedStepIds);return Object.keys(journal.compensationData).some(stepId=>!started.has(stepId))||Object.keys(journal.results).some(stepId=>!completed.has(stepId))?!1:journal.status==="committed"?stepIds.every(stepId=>completed.has(stepId)):journal.status!=="compensated"||completed.size===0&&started.size===0}function invalidJournal(transactionId,detail){return new PmCliError(`Workspace transaction ${transactionId} journal is invalid (${detail})`,EXIT_CODE.CONFLICT,{code:"transaction_journal_invalid",required:"A valid durable journal is required before transaction recovery can continue.",why:"Corrupt journal data cannot establish which steps applied or still require compensation.",nextSteps:["Preserve the journal and inspect its integrity or restore a verified backup before retrying.","Do not switch to a new transaction id to bypass an unreadable journal; previously applied work may remain."]})}function journalPlanMismatch(transactionId){return new PmCliError(`Workspace transaction ${transactionId} journal does not match the supplied plan`,EXIT_CODE.CONFLICT,{code:"transaction_plan_mismatch",required:"Replay the exact plan that started this transaction, or start a new transaction with a different id.",why:"A journal records the plan it is resuming; applying a different plan under the same id could skip or duplicate steps.",nextSteps:["Re-run with the original document to resume or replay it.","Pass --transaction-id <new-id> (or a new transactionId over MCP) to commit the changed plan as its own transaction.","Run pm ops gc --scope transactions --dry-run to inspect abandoned journals before pruning them."]})}function parseJournal(raw,transactionId,stepIds){let parsed;try{parsed=JSON.parse(raw)}catch{throw invalidJournal(transactionId,"invalid JSON")}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw invalidJournal(transactionId,"expected an object");const journal=parsed;if(!(journal.schemaVersion===1&&journal.transactionId===transactionId))throw invalidJournal(transactionId,"schema version or transaction id does not match the journal file");const recordedStepIds=journal.stepIds;if(!Array.isArray(recordedStepIds)||!hasUniqueKnownStepIds(recordedStepIds,recordedStepIds)||!hasValidJournalLifecycle(journal,recordedStepIds)||!hasValidJournalMetadata(journal)||typeof journal.status!="string"||!["applying","compensating","compensated","committed"].includes(journal.status))throw invalidJournal(transactionId,"invalid lifecycle, metadata, status or step ids");if(!(journal.status==="compensated"||recordedStepIds.length===stepIds.length&&recordedStepIds.every((stepId,index)=>stepId===stepIds[index])))throw journalPlanMismatch(transactionId);return{...journal,results:createResultMap(journal.results),compensationData:createResultMap(journal.compensationData)}}async function prepareJournalForApply(options,transactionId,author,stepIds,existing){let journal=existing;const recovered=journal!==void 0;if(journal?.status==="compensating"&&await compensateAppliedSteps(options,journal),journal===void 0||journal.status==="compensated"){const timestamp=nowIso();journal={schemaVersion:1,transactionId,author,status:"applying",attempt:(journal?.attempt??0)+1,createdAt:journal?.createdAt??timestamp,updatedAt:timestamp,stepIds,startedStepIds:[],completedStepIds:[],results:createResultMap(),compensationData:createResultMap()},await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"prepared")}return{journal,recovered}}async function applyPreparedTransaction(options,journal,recovered){try{await discoverAppliedSteps(journal,options.steps),await writeJournal(options.pmRoot,journal);const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of options.steps){if(completed.has(step.id))continue;const inspection=await step.inspect();let result=cloneOptionalJournalValue(inspection.result,`Step ${step.id} result`);if(inspection.state!=="applied"){const compensationData=await step.prepareCompensation?.();setJournalEntry(journal.compensationData,step.id,compensationData,`Step ${step.id} compensation data`),started.add(step.id),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),await writeJournal(options.pmRoot,journal);const appliedResult=await step.apply();result=cloneOptionalJournalValue(appliedResult,`Step ${step.id} result`)}await emitTransition(options,journal,"step_applied",step.id),completed.add(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),setJournalEntry(journal.results,step.id,result,`Step ${step.id} result`),await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_recorded",step.id)}return await emitTransition(options,journal,"committing"),journal.status="committed",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"committed"),{transactionId:journal.transactionId,status:"committed",recovered,results:createResultMap(journal.results)}}catch(error){if(error instanceof WorkspaceTransactionInterruptedError||journal.status==="committed")throw error;try{await compensateAppliedSteps(options,journal)}catch(compensationError){throw new AggregateError([error,compensationError],`Workspace transaction failed and compensation failed: ${String(compensationError)}`,{cause:compensationError})}throw error}}async function loadJournal(pmRoot,transactionId,stepIds){try{return parseJournal(await readFile(journalPath(pmRoot,transactionId),"utf8"),transactionId,stepIds)}catch(error){if(error!==null&&typeof error=="object"&&"code"in error&&isFileMissingError(error))return;throw error}}async function emitTransition(options,journal,transition,stepId){await options.onTransition?.({transactionId:journal.transactionId,attempt:journal.attempt,transition,...stepId===void 0?{}:{stepId}})}async function discoverAppliedSteps(journal,steps){const completed=new Set,results=createResultMap();for(const step of steps){const inspection=await step.inspect();inspection.state==="applied"&&(completed.add(step.id),setJournalEntry(results,step.id,inspection.result,`Step ${step.id} result`))}journal.results=results,journal.completedStepIds=steps.map(step=>step.id).filter(stepId=>completed.has(stepId))}async function compensateAppliedSteps(options,journal){journal.status="compensating",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensating");const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of[...options.steps].reverse()){if(!started.has(step.id)&&!completed.has(step.id))continue;let shouldCompensate;try{const state=(await step.inspect()).state;shouldCompensate=started.has(step.id)&&(state==="applied"||state==="pending"&&step.compensatePending===!0)}catch{shouldCompensate=started.has(step.id)}shouldCompensate&&(await emitTransition(options,journal,"step_compensating",step.id),await step.compensate(journal.compensationData[step.id])),completed.delete(step.id),started.delete(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),delete journal.results[step.id],delete journal.compensationData[step.id],await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_compensated",step.id)}journal.status="compensated",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensated")}async function commitWorkspaceTransaction(options){const{pmRoot,transactionId,author}=validateWorkspaceTransactionIdentity(options),lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),steps=[...options.steps];if(steps.length===0)throw new TypeError("Workspace transaction requires at least one step");const stepIds=steps.map(step=>{const stepId=requiredIdentifier(step.id,"Transaction step id");if(stepId!==step.id)throw new TypeError("Transaction step id must not contain surrounding whitespace");return stepId});if(new Set(stepIds).size!==stepIds.length)throw new TypeError("Workspace transaction step ids must be unique");const stableOptions={...options,pmRoot,transactionId,author,steps,lockTtlSeconds,lockWaitMs},release=await acquireLock(pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs);try{const existing=await loadJournal(pmRoot,transactionId,stepIds),recovered=existing!==void 0;if(existing?.status==="committed")return{transactionId,status:"committed",recovered:!0,results:createResultMap(existing.results)};const prepared=await prepareJournalForApply(stableOptions,transactionId,author,stepIds,existing);return await applyPreparedTransaction(stableOptions,prepared.journal,recovered||prepared.recovered)}finally{await release()}}export{WorkspaceTransactionInterruptedError,commitWorkspaceTransaction,validateWorkspaceTransactionIdentity};
1
+ (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a8daf042-f74c-5f51-b7e8-f8ff4fb8fe8e")}catch{}})();import{mkdir,readFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import{appendWorkspaceAuditEvent,getWorkspaceHistoryPath,WORKSPACE_HISTORY_ID}from"../core/history/workspace-history.js";import{readHistoryEntries}from"./history-read.js";import{acquireLock}from"../core/lock/lock.js";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{nowIso}from"../core/shared/time.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";class WorkspaceTransactionInterruptedError extends Error{constructor(message="Workspace transaction interrupted"){super(message),this.name="WorkspaceTransactionInterruptedError"}}const WORKSPACE_TRANSACTION_LOCK_ID="sdk-workspace-transaction",TRANSACTION_ID_PATTERN=/^[a-zA-Z0-9._-]+$/,DEFAULT_LOCK_TTL_SECONDS=30,DEFAULT_LOCK_WAIT_MS=3e3;function requiredIdentifier(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized||!TRANSACTION_ID_PATTERN.test(normalized))throw new TypeError(`${label} must match [a-zA-Z0-9._-]+`);return normalized}function requiredText(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized)throw new TypeError(`${label} must be non-empty`);return normalized}function validateWorkspaceTransactionIdentity(options){return{pmRoot:requiredText(options.pmRoot,"Transaction pmRoot"),transactionId:requiredIdentifier(options.transactionId,"Transaction id"),author:requiredText(options.author,"Transaction author")}}function positiveInteger(value,fallback,label){const resolved=value??fallback;if(!Number.isInteger(resolved)||resolved<=0)throw new TypeError(`${label} must be a positive integer`);return resolved}function journalPath(pmRoot,transactionId){return path.join(pmRoot,"transactions","sdk",`${transactionId}.json`)}async function writeJournal(pmRoot,journal){journal.updatedAt=nowIso();const target=journalPath(pmRoot,journal.transactionId);await mkdir(path.dirname(target),{recursive:!0}),await writeFileAtomic(target,`${JSON.stringify(journal,null,2)}
2
+ `)}function hasValidJournalMetadata(journal){return Number.isInteger(journal.attempt)&&typeof journal.author=="string"&&typeof journal.createdAt=="string"&&typeof journal.updatedAt=="string"&&hasValidAbandonment(journal)}function hasValidAbandonment(journal){const retirement=journal.abandonment;return retirement===void 0?journal.schemaVersion===1&&journal.status!=="abandoned":journal.schemaVersion!==2||retirement===null||typeof retirement!="object"||journal.status==="committed"?!1:typeof retirement.reason=="string"&&retirement.reason.trim().length>0&&typeof retirement.author=="string"&&retirement.author.trim().length>0&&(retirement.supersededBy===void 0||typeof retirement.supersededBy=="string"&&TRANSACTION_ID_PATTERN.test(retirement.supersededBy)&&retirement.supersededBy!==journal.transactionId)}function createResultMap(source){return Object.assign(Object.create(null),source)}function cloneJournalValue(value,label){try{const serialized=JSON.stringify(value,(_key,nestedValue)=>{if(typeof nestedValue=="number"&&!Number.isFinite(nestedValue))throw new TypeError(`${label} contains a non-finite number`);if(nestedValue===void 0||typeof nestedValue=="function"||typeof nestedValue=="symbol")throw new TypeError(`${label} contains a non-JSON value`);return nestedValue});return JSON.parse(serialized)}catch(error){throw new TypeError(`${label} must be JSON serializable`,{cause:error})}}function cloneOptionalJournalValue(value,label){return value===void 0?void 0:cloneJournalValue(value,label)}function setJournalEntry(target,stepId,value,label){const cloned=cloneOptionalJournalValue(value,label);cloned===void 0?delete target[stepId]:target[stepId]=cloned}function isJournalMap(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function hasUniqueKnownStepIds(value,stepIds){return Array.isArray(value)&&new Set(value).size===value.length&&value.every(stepId=>typeof stepId=="string"&&stepIds.includes(stepId))}function hasValidJournalLifecycle(journal,stepIds){if(!hasUniqueKnownStepIds(journal.startedStepIds,stepIds)||!hasUniqueKnownStepIds(journal.completedStepIds,stepIds)||!isJournalMap(journal.results)||!isJournalMap(journal.compensationData))return!1;const startedStepIds=journal.startedStepIds,started=new Set(startedStepIds),completedStepIds=journal.completedStepIds,completed=new Set(completedStepIds);return Object.keys(journal.compensationData).some(stepId=>!started.has(stepId))||Object.keys(journal.results).some(stepId=>!completed.has(stepId))?!1:journal.status==="committed"?stepIds.every(stepId=>completed.has(stepId)):!["compensated","abandoned"].includes(journal.status??"")||completed.size===0&&started.size===0}function invalidJournal(transactionId,detail){return new PmCliError(`Workspace transaction ${transactionId} journal is invalid (${detail})`,EXIT_CODE.CONFLICT,{code:"transaction_journal_invalid",required:"A valid durable journal is required before transaction recovery can continue.",why:"Corrupt journal data cannot establish which steps applied or still require compensation.",nextSteps:["Preserve the journal and inspect its integrity or restore a verified backup before retrying.","Do not switch to a new transaction id to bypass an unreadable journal; previously applied work may remain."]})}function journalPlanMismatch(transactionId){return new PmCliError(`Workspace transaction ${transactionId} journal does not match the supplied plan`,EXIT_CODE.CONFLICT,{code:"transaction_plan_mismatch",required:"Replay the original plan or safely abandon the interrupted transaction before replacing it.",why:"A journal records the plan it is resuming; applying a different plan under the same id could skip or duplicate steps.",nextSteps:["Re-run with the original document to resume or replay it.","For item batches use client.itemTransactions.status(transactionId), then client.itemTransactions.abandon(transactionId, { reason }) before committing a changed plan with a new transactionId.","Run pm ops gc --scope transactions --dry-run to inspect retention; active and explicitly abandoned journals remain protected."]})}function matchesJournalSteps(journal,recorded,requested){return requested===void 0||journal.abandonment!==void 0||journal.status==="compensated"||recorded.length===requested.length&&recorded.every((id,index)=>id===requested[index])}function parseJournal(raw,transactionId,stepIds){let parsed;try{parsed=JSON.parse(raw)}catch{throw invalidJournal(transactionId,"invalid JSON")}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw invalidJournal(transactionId,"expected an object");const journal=parsed;if(!((journal.schemaVersion===1||journal.schemaVersion===2)&&journal.transactionId===transactionId))throw invalidJournal(transactionId,"schema version or transaction id does not match the journal file");const recordedStepIds=journal.stepIds;if(!Array.isArray(recordedStepIds)||!hasUniqueKnownStepIds(recordedStepIds,recordedStepIds)||!hasValidJournalLifecycle(journal,recordedStepIds)||!hasValidJournalMetadata(journal)||typeof journal.status!="string"||!["applying","compensating","compensated","committed","abandoned"].includes(journal.status))throw invalidJournal(transactionId,"invalid lifecycle, metadata, status or step ids");if(!matchesJournalSteps(journal,recordedStepIds,stepIds))throw journalPlanMismatch(transactionId);return{...journal,results:createResultMap(journal.results),compensationData:createResultMap(journal.compensationData)}}async function prepareJournalForApply(options,transactionId,author,stepIds,existing){let journal=existing;const recovered=journal!==void 0;if(journal?.status==="compensating"&&await compensateAppliedSteps(options,journal),journal===void 0||journal.status==="compensated"){const timestamp=nowIso();journal={schemaVersion:1,transactionId,author,status:"applying",attempt:(journal?.attempt??0)+1,createdAt:journal?.createdAt??timestamp,updatedAt:timestamp,stepIds,startedStepIds:[],completedStepIds:[],results:createResultMap(),compensationData:createResultMap(),...options.recoveryData===void 0?{}:{recoveryData:cloneJournalValue(options.recoveryData,"Transaction recovery data")}},await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"prepared")}return{journal,recovered}}async function applyPreparedTransaction(options,journal,recovered){try{await discoverAppliedSteps(journal,options.steps),await writeJournal(options.pmRoot,journal);const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of options.steps){if(completed.has(step.id))continue;const inspection=await step.inspect();let result=cloneOptionalJournalValue(inspection.result,`Step ${step.id} result`);if(inspection.state!=="applied"){const compensationData=await step.prepareCompensation?.();setJournalEntry(journal.compensationData,step.id,compensationData,`Step ${step.id} compensation data`),started.add(step.id),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),await writeJournal(options.pmRoot,journal);const appliedResult=await step.apply();result=cloneOptionalJournalValue(appliedResult,`Step ${step.id} result`)}await emitTransition(options,journal,"step_applied",step.id),completed.add(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),setJournalEntry(journal.results,step.id,result,`Step ${step.id} result`),await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_recorded",step.id)}return await emitTransition(options,journal,"committing"),journal.status="committed",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"committed"),{transactionId:journal.transactionId,status:"committed",recovered,results:createResultMap(journal.results)}}catch(error){if(error instanceof WorkspaceTransactionInterruptedError||journal.status==="committed")throw error;try{await compensateAppliedSteps(options,journal)}catch(compensationError){throw new AggregateError([error,compensationError],`Workspace transaction failed and compensation failed: ${String(compensationError)}`,{cause:compensationError})}throw error}}async function loadJournal(pmRoot,transactionId,stepIds){try{return parseJournal(await readFile(journalPath(pmRoot,transactionId),"utf8"),transactionId,stepIds)}catch(error){if(error!==null&&typeof error=="object"&&"code"in error&&isFileMissingError(error))return;throw error}}async function emitTransition(options,journal,transition,stepId){await options.onTransition?.({transactionId:journal.transactionId,attempt:journal.attempt,transition,...stepId===void 0?{}:{stepId}})}async function discoverAppliedSteps(journal,steps){const completed=new Set,results=createResultMap();for(const step of steps){const inspection=await step.inspect();inspection.state==="applied"&&(completed.add(step.id),setJournalEntry(results,step.id,inspection.result,`Step ${step.id} result`))}journal.results=results,journal.completedStepIds=steps.map(step=>step.id).filter(stepId=>completed.has(stepId))}async function compensateAppliedSteps(options,journal){journal.status="compensating",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensating");const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of[...options.steps].reverse()){if(!started.has(step.id)&&!completed.has(step.id))continue;let shouldCompensate;try{const state=(await step.inspect()).state;shouldCompensate=started.has(step.id)&&(state==="applied"||state==="pending"&&step.compensatePending===!0)}catch{shouldCompensate=started.has(step.id)}shouldCompensate&&(await emitTransition(options,journal,"step_compensating",step.id),await step.compensate(journal.compensationData[step.id])),completed.delete(step.id),started.delete(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),delete journal.results[step.id],delete journal.compensationData[step.id],await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_compensated",step.id)}journal.status="compensated",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensated")}async function commitWorkspaceTransaction(options){const{pmRoot,transactionId,author}=validateWorkspaceTransactionIdentity(options),lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),steps=[...options.steps];if(steps.length===0)throw new TypeError("Workspace transaction requires at least one step");const stepIds=steps.map(step=>{const stepId=requiredIdentifier(step.id,"Transaction step id");if(stepId!==step.id)throw new TypeError("Transaction step id must not contain surrounding whitespace");return stepId});if(new Set(stepIds).size!==stepIds.length)throw new TypeError("Workspace transaction step ids must be unique");const stableOptions={...options,pmRoot,transactionId,author,steps,lockTtlSeconds,lockWaitMs},release=await acquireLock(pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs);try{const existing=await loadJournal(pmRoot,transactionId,stepIds);if(existing?.status==="abandoned"||existing?.abandonment!==void 0)throw retiredTransaction(existing);const recovered=existing!==void 0;if(existing?.status==="committed")return{transactionId,status:"committed",recovered:!0,results:createResultMap(existing.results)};const prepared=await prepareJournalForApply(stableOptions,transactionId,author,stepIds,existing);return await applyPreparedTransaction(stableOptions,prepared.journal,recovered||prepared.recovered)}finally{await release()}}function retiredTransaction(journal){const pending=journal.status!=="abandoned",supersededBy=journal.abandonment.supersededBy,retry=supersededBy===void 0?`Retry abandonment without supersededBy to finish retirement of ${JSON.stringify(journal.transactionId)}.`:`Retry abandonment with supersededBy ${JSON.stringify(supersededBy)} to finish retirement of ${JSON.stringify(journal.transactionId)}.`;return new PmCliError(pending?`Transaction ${journal.transactionId} retirement is incomplete`:`Transaction ${journal.transactionId} has been abandoned`,EXIT_CODE.CONFLICT,{code:"transaction_abandoned",required:pending?retry:"Use a new transactionId for the replacement plan.",why:pending?"Forward replay is fenced, but the recorded retirement has not finished. Preserve its replacement identity.":"An explicitly retired transaction identity cannot be resumed.",...pending?{retirement:{state:"pending",superseded_by:supersededBy??null},nextSteps:[retry]}:{}})}async function readWorkspaceTransaction(pmRoot,transactionId){const identity=validateWorkspaceTransactionIdentity({pmRoot,transactionId,author:"reader"});return loadJournal(identity.pmRoot,identity.transactionId)}function workspaceTransactionPlanDigest(journal){return createHash("sha256").update(JSON.stringify(journal.stepIds)).digest("hex")}async function abandonWorkspaceTransaction(options){const identity=validateWorkspaceTransactionIdentity(options),reason=requiredText(options.reason,"Abandonment reason"),supersededBy=options.supersededBy===void 0?void 0:requiredIdentifier(options.supersededBy,"Replacement transaction id");if(supersededBy===identity.transactionId)throw new TypeError("Replacement transaction id must differ from the abandoned id");const lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),release=await acquireLock(identity.pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,identity.author,!1,!1,lockWaitMs);try{const journal=await loadJournal(identity.pmRoot,identity.transactionId);if(journal===void 0)throw new PmCliError(`Transaction ${identity.transactionId} was not found`,EXIT_CODE.NOT_FOUND);if(journal.status==="committed")throw new PmCliError(`Transaction ${identity.transactionId} is committed and cannot be abandoned`,EXIT_CODE.CONFLICT,{code:"transaction_committed",required:"Plan a separate explicit corrective mutation.",why:"Recovery cannot silently undo committed work."});if(journal.abandonment!==void 0&&journal.abandonment.supersededBy!==supersededBy)throw retiredTransaction(journal);if(journal.status==="abandoned")return journal;const steps=options.resolveSteps(structuredClone(journal));if(JSON.stringify(steps.map(step=>step.id))!==JSON.stringify(journal.stepIds))throw journalPlanMismatch(identity.transactionId);if(journal.abandonment===void 0){if(supersededBy!==void 0&&await loadJournal(identity.pmRoot,supersededBy)!==void 0)throw new PmCliError(`Replacement transaction ${supersededBy} already exists`,EXIT_CODE.CONFLICT);journal.schemaVersion=2,journal.abandonment={reason,author:identity.author,...supersededBy===void 0?{}:{supersededBy}},await writeJournal(identity.pmRoot,journal)}return journal.status!=="compensated"&&await compensateAppliedSteps({...options,...identity,steps},journal),await auditTransactionAbandonment(journal,identity.pmRoot,lockTtlSeconds,lockWaitMs),journal.status="abandoned",await writeJournal(identity.pmRoot,journal),journal}finally{await release()}}async function auditTransactionAbandonment(journal,pmRoot,lockTtlSeconds,lockWaitMs){const planDigest=workspaceTransactionPlanDigest(journal);if((await readHistoryEntries(getWorkspaceHistoryPath(pmRoot),WORKSPACE_HISTORY_ID)).some(entry=>entry.op==="transaction_abandon"&&entry.context?.transaction_id===journal.transactionId&&entry.context?.attempt===journal.attempt&&entry.context?.plan_digest===planDigest))return;const abandonment=journal.abandonment;await appendWorkspaceAuditEvent({pmRoot,author:abandonment.author,lockTtlSeconds,lockWaitMs,op:"transaction_abandon",message:abandonment.reason,context:{transaction_id:journal.transactionId,attempt:journal.attempt,plan_digest:planDigest,...abandonment.supersededBy===void 0?{}:{superseded_by:abandonment.supersededBy}}})}export{WorkspaceTransactionInterruptedError,abandonWorkspaceTransaction,commitWorkspaceTransaction,readWorkspaceTransaction,validateWorkspaceTransactionIdentity,workspaceTransactionPlanDigest};
3
3
  //# sourceMappingURL=workspace-transaction.js.map
package/docs/COMMANDS.md CHANGED
@@ -1182,16 +1182,21 @@ unowned hooks. Default installation remains hook-free. See
1182
1182
 
1183
1183
  ## History and Recovery
1184
1184
 
1185
- Compact and provenance history without `--limit` display newest entries first,
1186
- so the latest handoff survives an output budget. The `order` receipt declares
1187
- the display direction and `total` counts all matching entries before limits.
1188
- Physical `index` values and durable version addresses retain their original
1189
- meaning. Use the returned `output_budget_truncation` continuation with
1190
- `--output-cursor` to read older rows without gaps or duplicates. Explicit
1191
- `--limit N` keeps the latest N entries in chronological order; `--full
1192
- --output-budget unbounded` returns complete chronological entries. Verification
1193
- always checks the full authoritative chain
1194
- ([pm-gh1605](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1605.toon); tracker record excluded from npm).
1185
+ Compact and provenance history default to newest-first, including reads with
1186
+ `--limit`. Full entries default to oldest-first. Use `--order oldest_first` or
1187
+ `--order newest_first` to choose the same direction for any projection and its
1188
+ value diffs. `--limit N` selects the latest N matching entries before ordering.
1189
+ The `order` receipt declares direction and `total` counts matches before limits.
1190
+ Physical indices and durable versions retain their original meaning. Use
1191
+ `--output-cursor` with the returned continuation to traverse the selected order.
1192
+ Verification always checks the full authoritative chain.
1193
+
1194
+ Compatibility: a compact/provenance latest-N read used to flip to chronological
1195
+ order. Chronological consumers should explicitly use, for example,
1196
+ `pm history pm-a1b2 --diff --limit 20 --order oldest_first`.
1197
+ Full reads keep their chronological default.
1198
+ Tracked by [pm-gh1617](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1617.toon)
1199
+ (tracker record excluded from npm).
1195
1200
 
1196
1201
  After a branch merge that touched tracker history, preview and apply the
1197
1202
  one-command reconciliation gate:
@@ -120,8 +120,9 @@ primary rows while a rich result can name
120
120
  `checks.0.details.missing_resolution_rows` as a continuation row. This prevents
121
121
  an inner diagnostic array from disabling `--output-limit` on the outer checks.
122
122
  `toon_encoding: "tabular_when_uniform"` declares that an array of flat objects
123
- with one shared key set renders as a length-marked TOON table; mixed, nested,
124
- or heterogeneous arrays retain the expanded representation. Quoted,
123
+ with one shared key set can render as a length-marked TOON table. Nested or
124
+ heterogeneous object rows can also use a smaller table with explicit sibling
125
+ encoding metadata; otherwise they retain the expanded representation. Quoted,
125
126
  separator-bearing, and multiline values use the canonical TOON encoder and
126
127
  round-trip through the strict decoder.
127
128
  Commands without a row collection, including a dependency tree or leaf `get`,
@@ -463,3 +464,24 @@ Continuation and compaction slice these receipts with the rows. Self-describing
463
464
  focus rows need no extra receipt. Semantic receipts are preserved during
464
465
  explanatory-string compaction, and corrupt alphabet or row-count mismatches are
465
466
  rejected.
467
+
468
+
469
+ ### Compact nested table metadata
470
+
471
+ Tracked by [pm-gh1603](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1603.toon)
472
+ (tracker record excluded from npm). The SDK `encodePmTableRows(rows, { compact: true })`
473
+ can declare `list_columns` (column to delimiter) and `null_columns` alongside
474
+ `json_columns` and `absent`. Omitted null columns also carry `column_order`
475
+ to restore their original first-occurrence positions. `decodePmTableRows` restores
476
+ these facts exactly.
477
+ A safe string-array column uses `;` between nonempty members; an empty cell means
478
+ an empty array. Any empty member, delimiter collision, mixed type or nested value
479
+ uses the existing JSON-column encoding instead. Null columns are omitted only
480
+ when every row explicitly contains null; missing fields remain distinct.
481
+
482
+ The TOON host selects compact tables only when they reduce rendered bytes.
483
+ Agenda rows therefore share null-field declarations and tag arrays avoid repeated
484
+ JSON escaping. JSON output and SDK query results retain their original values.
485
+ Consumers of TOON tables must honor all declared encoding metadata or use the
486
+ current SDK decoder. Whole-envelope budgets measure the actual serialized form,
487
+ including decoding metadata.
package/docs/SDK.md CHANGED
@@ -453,15 +453,22 @@ Promoted Plan links retain their graph semantics: `depends_on` canonicalizes to
453
453
 
454
454
  ### Immutable history and rich item reads
455
455
 
456
- `runHistory(id, { compact: true }, global)` and `{ provenance: true }` return
457
- newest-first rows when no `limit` is supplied. The CLI and `runAction` history
458
- defaults use compact rows. The result declares `order` and a complete matching
459
- `total`; original row indices and durable versions stay unchanged. Apply the
460
- public read-output dimensions and carry `outputCursor` from their truncation
461
- receipt to traverse older entries. An explicit latest-entry `limit` retains
462
- chronological order, as do full SDK reads (`runHistory(id, {}, global)`) and
463
- `runAction` with `full: true`. Verification still consumes the full chain
464
- ([pm-gh1605](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1605.toon); tracker record excluded from npm).
456
+ `runHistory(id, { compact: true }, global)` and `{ provenance: true }` default
457
+ to newest-first rows, with or without `limit`. Full SDK reads (`runHistory(id,
458
+ {}, global)`) and `runAction` with `full: true` default to chronological rows.
459
+ Set `order: "oldest_first"` or `order: "newest_first"` explicitly to use the same
460
+ direction across every projection, including value diffs. `limit` selects the
461
+ latest N matching entries before ordering; it never changes the direction.
462
+ CLI and `runAction` defaults use compact rows. The result declares `order` and
463
+ a complete matching `total`; physical indices and durable versions stay unchanged.
464
+ Read-output continuations traverse the chosen order without changing it.
465
+ Verification and value-diff reconstruction still consume the authoritative chain.
466
+
467
+ Compatibility: compact/provenance reads with a limit previously returned
468
+ chronological rows. Replay consumers must now request `order: "oldest_first"`
469
+ (or CLI `--order oldest_first`) explicitly. Full reads retain their prior default.
470
+ Tracked by [pm-gh1617](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1617.toon)
471
+ (tracker record excluded from npm).
465
472
 
466
473
  Tracked: [pm-4a7m](../.agents/pm/features/pm-4a7m.toon),
467
474
  [pm-hib1](../.agents/pm/features/pm-hib1.toon),
@@ -1166,6 +1173,46 @@ recurrence reason, accepts only the workspace open or in-progress target, and
1166
1173
  returns the terminal evidence captured atomically before the update path clears
1167
1174
  it from active metadata.
1168
1175
 
1176
+ Literal mutation data and runtime value admission are tracked by
1177
+ [pm-gh1614](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1614.toon)
1178
+ and [pm-gh1606](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1606.toon)
1179
+ (repository-only tracker records excluded from npm).
1180
+ SDK `create`, `update`, and `append` inputs are data: `"-"` remains text,
1181
+ `"none"` and `"null"` remain text where the field's domain permits them, and
1182
+ `"@file"` remains text. These calls do not implicitly consume stdin or read a
1183
+ file. This also applies to the corresponding `runCreate`, `runUpdate`,
1184
+ `runAppend` operations and update-many mutation values. Normal field validation,
1185
+ whitespace handling, structured collection syntax, and project schemas still
1186
+ apply. Clear fields explicitly with `unset: ["assignee"]` or the declared
1187
+ `clear*` collection options; JavaScript `null` is not an unset directive.
1188
+ CLI adapters retain `--description -`, append stdin, file flags, and legacy
1189
+ clear-token compatibility. Values inside CLI `--stdin-json` documents are
1190
+ literal data, including `"none"` and `"null"`. Explicit CLI flags override
1191
+ document values and retain CLI interpretation, including legacy clear tokens.
1192
+ Use explicit clear/unset flags when the intent is to remove a field.
1193
+
1194
+ Native create/update/reopen values are checked against the SDK-owned option contracts
1195
+ before normalization and writes. A malformed append body or option value throws
1196
+ `PmCliError` with `code: "invalid_argument_value"`, usage exit code 2, the
1197
+ affected `context.field`, corrective guidance, and `context.changed: false`.
1198
+ For example, `estimatedMinutes` requires `"45"`, while `priority` accepts a
1199
+ string or finite number. JavaScript and JSON bridge callers must correct invalid
1200
+ types instead of relying on coercion. Integrations that previously used `"-"`
1201
+ to request stdin should read that input themselves and pass the resulting text;
1202
+ integrations that used `"none"` to clear must migrate to `unset`.
1203
+ Direct `runCreate`/`runUpdate` calls require the declared string arrays for
1204
+ repeatable fields. Generic action dispatch retains its documented scalar-to-array
1205
+ normalization before invoking those operations.
1206
+
1207
+ Adapter authors can import `withCliMutationInput` and
1208
+ `withLiteralMutationInput` from the SDK root or runtime entrypoint. Both return
1209
+ cloned option bags and collection values with an in-process input policy. Use
1210
+ the first only for explicit CLI parsing, and the second for decoded JSON or
1211
+ other literal data inside that adapter. Existing literal data and already-resolved stdin fields
1212
+ retain their policy through the clone; no marker is serialized into an item.
1213
+ Reusing a collection in a fresh CLI option bag does not inherit literal policy
1214
+ from another call or modify the caller's collection.
1215
+
1169
1216
  Create focus policy is tracked by [pm-gh1577](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1577.toon)
1170
1217
  (this tracker record is repository-only and excluded from npm).
1171
1218
  Programmatic `create`, `PmClient.create`, `runCreate`, and MCP `pm_create`
@@ -1602,6 +1649,84 @@ the same durable coordinator, restoring annotations, linked artifacts,
1602
1649
  lifecycle fields, and claim ownership on failure. It is the SDK primitive
1603
1650
  behind `pm item complete`.
1604
1651
 
1652
+ Host-bound batch and recovery APIs are tracked by
1653
+ [pm-gh1598](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/features/pm-gh1598.toon) and
1654
+ [pm-gh1597](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/features/pm-gh1597.toon)
1655
+ (tracker records excluded from npm). A `PmClient` acquired from an
1656
+ extension host supports the same batch coordinator without reconstructing its
1657
+ tracker root or overriding the host author policy:
1658
+
1659
+ ```ts
1660
+ const plan = { transactionId: "sync-42", mutations };
1661
+ const preview = await client.previewItemMutations(plan);
1662
+ await client.commitItemMutations(plan);
1663
+ const receipt = await client.itemTransactions.status("sync-42");
1664
+ ```
1665
+
1666
+ Preview and status setup use configuration-only source reads, so activation
1667
+ does not open optional schema files before preview validates their storage.
1668
+
1669
+ Each mutation in a newly staged batch contributes a `records` row with its
1670
+ original `index`, canonical `id`, operation, and full public `item` projection
1671
+ including body after the complete batch. Repeated targets therefore show the
1672
+ same final staged state. `execution` distinguishes a newly `applied` mutation
1673
+ from `adopted` existing state; adopted values do not certify the unused create
1674
+ options. `commit_time_fields` lists JSON pointers to potentially
1675
+ regenerated clocks, conservatively including existing or explicitly supplied
1676
+ clocks. These preview values are observations, not reserved commit values.
1677
+ Compare normalized intent separately from those clocks; commit still evaluates
1678
+ current state, attribution, extension policy and concurrency. A committed replay returns an empty
1679
+ `records` array, not a reconstruction of its original write values. While the
1680
+ committed journal is retained, replaying the identical plan performs no item
1681
+ write; changing its ordered payload under the same id refuses with
1682
+ `transaction_plan_mismatch`. If transaction GC removes that committed journal,
1683
+ the coordinator can treat its id as new; replay guarantees do not survive that
1684
+ removal.
1685
+
1686
+ The workspace transaction lock serializes transaction coordinators. Ordinary
1687
+ SDK and CLI item writers can still interleave between a step's read and write;
1688
+ the lock does not provide a general read-decide-write critical section.
1689
+
1690
+ New item journals retain their original mutation plan and compensation policy.
1691
+ History redaction does not rewrite transaction journals: values removed from
1692
+ item history can remain in interrupted or abandoned journals. GC retains those
1693
+ recovery receipts. Do not include secrets in batch mutations.
1694
+ The status receipt contains the validated journal and a SHA-256 `planDigest`
1695
+ over its ordered payload-bound step identifiers. Started/completed lists report
1696
+ persisted coordinator progress: a crash after apply but before recording can
1697
+ leave an applied step listed only as started. Recovery re-inspects durable item
1698
+ history before compensation. No retained journal returns `undefined`; corrupt
1699
+ journals refuse instead of appearing absent.
1700
+
1701
+ ```ts
1702
+ await client.itemTransactions.supersede("interrupted-sync", "sync-43", {
1703
+ reason: "Re-plan against the current item revision",
1704
+ });
1705
+ // Read current items and build the replacement only after retirement succeeds.
1706
+ await client.commitItemMutations({ transactionId: "sync-43", mutations: replanned });
1707
+ ```
1708
+
1709
+ `abandon(id, { reason })` durably fences forward replay before compensating
1710
+ interrupted owned effects in reverse order,
1711
+ records a workspace audit event, and marks the identity `abandoned`. Retirement
1712
+ upgrades the journal to schema version 2 so older coordinators refuse the pending
1713
+ intent instead of silently resuming it.
1714
+ `supersede(id, newId, { reason })` performs that retirement and records the
1715
+ replacement identity; it does not atomically commit the replacement plan or
1716
+ reserve its id. Repeating the same retirement is idempotent. Committed journals
1717
+ cannot be abandoned. A pending-retirement refusal keeps the
1718
+ `transaction_abandoned` code, reports `retirement.state: "pending"` and the
1719
+ recorded `superseded_by` value (`null` for abandonment without a replacement),
1720
+ and explains how to retry the original intent. Finish that retirement before
1721
+ re-planning. During compensation, a later peer edit refuses with
1722
+ `transaction_partial_mutation` and retains recovery data; it is never silently
1723
+ overwritten. Legacy journals without a retained item plan require the original
1724
+ plan and adapter for replay. Abandoned receipts remain retained by transaction
1725
+ GC, including pending retirement intents, so their identities cannot silently
1726
+ become reusable. Mutation inputs are copied before coordinator admission; later
1727
+ caller edits cannot change the payload whose identity and recovery plan were
1728
+ recorded.
1729
+
1605
1730
  `previewItemMutations` validates an ordered plan with the same transaction
1606
1731
  coordinator and lifecycle primitives as apply, using a disposable copy of the
1607
1732
  tracker. Earlier staged creates, evidence updates and closures participate in
package/docs/TESTING.md CHANGED
@@ -80,6 +80,14 @@ policy-check budget. This allocation is tracked by
80
80
  [pm-a3c6g4](../.agents/pm/tasks/pm-a3c6g4.toon); broader runner qualification
81
81
  remains with [pm-bj7rq0](../.agents/pm/issues/pm-bj7rq0.toon).
82
82
 
83
+ The same cohort runs the complete cold transport-floor and executable refusal
84
+ gate fixture files after parallel contracts
85
+ ([pm-serial-cold-gates](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-serial-cold-gates.toon),
86
+ a repository-only tracker record excluded from npm). Their original 30-second
87
+ and 360-second test deadlines, assertions, negative controls, retries, and product
88
+ benchmark thresholds are unchanged. Isolated passes qualify this allocation;
89
+ they do not establish passing product latency or replace complete-suite proof.
90
+
83
91
  The same serial cohort runs the ten-thousand-item aggregation, cold governance
84
92
  read, and surface-replication fixture files after parallel contracts
85
93
  ([pm-ckb8w5](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-ckb8w5.toon),
@@ -32,7 +32,7 @@ This file is generated by `listPmFlagLexicon()`. Compatibility aliases do not co
32
32
  | `duplicates` | context | 25 | 25 |
33
33
  | `eval` | context | 25 | 25 |
34
34
  | `next` | context | 38 | 38 |
35
- | `history` | evidence | 35 | 35 |
35
+ | `history` | evidence | 36 | 36 |
36
36
  | `events` | evidence | 34 | 34 |
37
37
  | `history-attest` | evidence | 23 | 23 |
38
38
  | `history-redact` | evidence | 27 | 27 |