@unbrained/pm-cli 2026.10.15 → 2026.10.17
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.
- package/.agents/skills/pm-developer/references/COMMAND_PLAYBOOK.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +20 -0
- package/dist/cli/extension-command-options.d.ts +3 -1
- package/dist/cli/extension-command-options.js +1 -1
- package/dist/cli/help-content.js +2 -2
- package/dist/cli/register-mutation.js +2 -2
- package/dist/cli/register-operations.js +1 -1
- package/dist/cli/register-structured-mutation.d.ts +0 -5
- package/dist/cli/register-structured-mutation.js +1 -1
- package/dist/cli/runtime/invocation-options.js +1 -1
- package/dist/cli-bundle/bundle-manifest.json +1 -1
- package/dist/cli-bundle/chunks/{chunk-7VHINXZP.js → chunk-3QKI3XNX.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-4PNPDT2N.js +236 -0
- package/dist/cli-bundle/chunks/{chunk-WBBW3WQX.js → chunk-6ZDKKOKM.js} +28 -28
- package/dist/cli-bundle/chunks/{chunk-LNKLQYBG.js → chunk-B5QVGCZX.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-52EZH2QQ.js → chunk-EA2CG7UM.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-HY7FQ7QX.js +2 -0
- package/dist/cli-bundle/chunks/{chunk-NC3QVGDD.js → chunk-J4LPQCET.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-JGKTOBVB.js → chunk-JS3B2M4D.js} +4 -4
- package/dist/cli-bundle/chunks/chunk-O4YBC3OM.js +30 -0
- package/dist/cli-bundle/chunks/{chunk-N56CAQBU.js → chunk-OZCSXX4D.js} +52 -52
- package/dist/cli-bundle/chunks/chunk-VMCSTFSO.js +2 -0
- package/dist/cli-bundle/chunks/chunk-W3KZ22DW.js +2 -0
- package/dist/cli-bundle/chunks/{chunk-RJ5N44WY.js → chunk-X2ONXFBP.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-7OEGNA4X.js → chunk-X53HM753.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-FKKFQ66D.js → register-list-query-RU3QZSAQ.js} +2 -2
- package/dist/cli-bundle/chunks/register-mutation-EQYMT32A.js +20 -0
- package/dist/cli-bundle/chunks/{register-operations-GEQ2H7PS.js → register-operations-VXBGE4XI.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-PSJTQBBO.js → register-setup-UUDEGFRO.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-TYLG5VEB.js → chunk-2IOHHNRT.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-PDYE7QDX.js → chunk-5JEFDEL3.js} +30 -30
- package/dist/cli-bundle/focused-chunks/{chunk-3RNUMVK4.js → chunk-AH6NX6GM.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-N6FUKTZS.js → chunk-E33QA7PK.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-4KQFMOM7.js → chunk-FTUZ3JF2.js} +5 -5
- package/dist/cli-bundle/focused-chunks/{chunk-5OO54BIV.js → chunk-JXR6UKFM.js} +3 -3
- package/dist/cli-bundle/focused-chunks/chunk-LDKX4KYJ.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-H3P77KVU.js → chunk-MBQF6HT6.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-5TBAVZDR.js → chunk-NZO7GOXD.js} +14 -14
- package/dist/cli-bundle/focused-chunks/{chunk-52I2SNKJ.js → chunk-PYC7BF6Z.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-THYQR3ON.js → chunk-QOSVJDOO.js} +59 -59
- package/dist/cli-bundle/focused-chunks/{chunk-FQKPOZKS.js → chunk-RBLSY6ML.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-Q4VNCTR2.js → chunk-RHTH5ILM.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-5KRACGBK.js → chunk-SIGIGZCG.js} +3 -3
- package/dist/cli-bundle/focused-chunks/chunk-TRFVAWF2.js +26 -0
- package/dist/cli-bundle/focused-chunks/{chunk-KIIFVULU.js → chunk-UNTQE37U.js} +2 -2
- package/dist/cli-bundle/main.js +27 -27
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +2 -2
- package/dist/cli-bundle/sdk-core.js +23 -23
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +54 -54
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +7 -7
- package/dist/core/extensions/extension-types.d.ts +15 -0
- package/dist/core/extensions/extension-types.js +1 -1
- package/dist/core/extensions/flag-value-types.d.ts +16 -0
- package/dist/core/extensions/flag-value-types.js +1 -1
- package/dist/core/extensions/registration-contracts.js +1 -1
- package/dist/core/item/item-format.js +3 -3
- package/dist/core/output/mutation-projection.d.ts +0 -21
- package/dist/core/output/mutation-projection.js +1 -1
- package/dist/core/schema/type-workflows.d.ts +1 -1
- package/dist/core/schema/type-workflows.js +1 -1
- package/dist/core/shared/text-normalization.d.ts +2 -0
- package/dist/core/shared/text-normalization.js +1 -1
- package/dist/core/store/item-mutation-precondition.d.ts +11 -0
- package/dist/core/store/item-mutation-precondition.js +2 -0
- package/dist/core/store/item-store.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tool-definitions.js +1 -1
- package/dist/sdk/cli-contracts/agent-output-contracts.js +1 -1
- package/dist/sdk/cli-contracts/flag-contracts.js +1 -1
- package/dist/sdk/cli-contracts/flag-lexicon-contracts.js +1 -1
- package/dist/sdk/cli-contracts/registration-helpers.d.ts +1 -1
- package/dist/sdk/cli-contracts/registration-helpers.js +1 -1
- package/dist/sdk/cli-contracts/tool-schema.d.ts +2 -2
- package/dist/sdk/cli-contracts/tool-schema.js +1 -1
- package/dist/sdk/context-intent-contracts.js +1 -1
- package/dist/sdk/core.d.ts +2 -1
- package/dist/sdk/core.js +1 -1
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +1 -1
- package/dist/sdk/guide-topics.js +1 -1
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/item-transaction.d.ts +72 -9
- package/dist/sdk/item-transaction.js +1 -1
- package/dist/sdk/lifecycle/claim.d.ts +15 -1
- package/dist/sdk/lifecycle/claim.js +1 -1
- package/dist/sdk/lifecycle/close.d.ts +2 -8
- package/dist/sdk/lifecycle/close.js +1 -1
- package/dist/sdk/lifecycle/core-item-fields.d.ts +6 -0
- package/dist/sdk/lifecycle/core-item-fields.js +2 -0
- package/dist/sdk/lifecycle/mcp-actions.js +1 -1
- package/dist/sdk/lifecycle/mutation-option-validation.d.ts +2 -0
- package/dist/sdk/lifecycle/mutation-option-validation.js +2 -0
- package/dist/sdk/lifecycle/release-handoff.d.ts +12 -0
- package/dist/sdk/lifecycle/release-handoff.js +2 -0
- package/dist/sdk/lifecycle/structured-mutation-plan.d.ts +33 -0
- package/dist/sdk/lifecycle/structured-mutation-plan.js +2 -0
- package/dist/sdk/lifecycle/task-composition.d.ts +2 -2
- package/dist/sdk/lifecycle/task-composition.js +1 -1
- package/dist/sdk/lifecycle/update.d.ts +3 -11
- package/dist/sdk/lifecycle/update.js +1 -1
- package/dist/sdk/lifecycle/workflow-transition.d.ts +13 -0
- package/dist/sdk/lifecycle/workflow-transition.js +2 -0
- package/dist/sdk/lifecycle-policy.d.ts +12 -2
- package/dist/sdk/lifecycle-policy.js +1 -1
- package/dist/sdk/public-surface.json +1 -1
- package/dist/sdk/read-output-budget.js +1 -1
- package/dist/sdk/runtime-input.js +1 -1
- package/dist/sdk/runtime-primitives.d.ts +1 -1
- package/dist/sdk/runtime-primitives.js +1 -1
- package/dist/sdk/runtime.d.ts +8 -8
- package/dist/sdk/runtime.js +1 -1
- package/dist/sdk/structured-mutations.d.ts +52 -1
- package/dist/sdk/structured-mutations.js +1 -1
- package/dist/sdk/workspace-transaction.js +2 -2
- package/docs/AGENT_GUIDE.md +33 -4
- package/docs/CLOSE_EVIDENCE_RECOVERY.md +16 -7
- package/docs/COMMANDS.md +85 -10
- package/docs/ECOSYSTEM_PLANNING_REVIEW.md +5 -1
- package/docs/EXTENSIONS.md +1 -1
- package/docs/SDK.md +114 -2
- package/docs/TESTING.md +15 -0
- package/docs/generated/FLAG_LEXICON_BUDGETS.md +1 -1
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +16 -3
- package/marketplace.json +1 -1
- package/package.json +2 -1
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-claude/package.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/pm-codex/package.json +1 -1
- package/plugins/pm-codex/plugin.json +1 -1
- package/runtime-dependencies.json +1 -1
- package/dist/cli-bundle/chunks/chunk-EMEEHV2Y.js +0 -2
- package/dist/cli-bundle/chunks/chunk-JSUJWIHQ.js +0 -30
- package/dist/cli-bundle/chunks/chunk-VIMQCFCK.js +0 -2
- package/dist/cli-bundle/chunks/chunk-X2H6UR26.js +0 -236
- package/dist/cli-bundle/chunks/register-mutation-EWMMVZJB.js +0 -20
- package/dist/cli-bundle/focused-chunks/chunk-HTA3TNC3.js +0 -26
- package/dist/cli-bundle/focused-chunks/chunk-JKBH7YGX.js +0 -2
- package/docs/ecosystem-review-2026-09-28.json +0 -1
|
@@ -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]="7599b1e6-1f28-597a-adda-ad856beb35bd")}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,ITEM_PROJECT_CONTEXT_KEYS}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{levenshteinDistanceWithinLimit}from"../core/shared/levenshtein.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{readRuntimeFieldOptionValue}from"../core/schema/runtime-field-values.js";const MUTATION_ROW_KEYS=["op","id","reason","options"],REFERENCED_MUTATION_ROW_KEYS=[...MUTATION_ROW_KEYS,"ref"],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"]),ITEM_FIELD_KEYS=new Set(["id","title","description","type","status","priority","tags","body","deadline","estimated_minutes","acceptance_criteria","definition_of_ready","order","rank","assignee",...ITEM_PROJECT_CONTEXT_KEYS,"affected_version","fixed_version","component","regression","customer_impact","close_reason","completed_at","dependencies","comments","notes","learnings","files","tests","docs","reminders","events","type_options"]),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 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){if(options!==void 0){if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} options must be an object.`,EXIT_CODE.USAGE);for(const key of Object.keys(options))if(!MUTATION_OPTION_KEYS[op].includes(key))throw unknownKeyError(`mutation ${index+1} ${op} options`,key,MUTATION_OPTION_KEYS[op]);return options}}function validateMutationRow(value,index){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);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}}function parseItemMutationBatch(input){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);return rows.map((row,index)=>{const mutation=validateMutationRow(row,index);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){const graph=new Map;for(const row of rows){if(row.op!=="create"||typeof row.ref!="string")continue;const mutationOptions=isPlainObject(row.options)?row.options:{},dependencies=Array.isArray(mutationOptions.dep)?mutationOptions.dep:mutationOptions.dep===void 0?[]:[mutationOptions.dep];graph.set(row.ref,[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 parseMutationDocumentRows(input){const parsed=parseJsonValue(input,"Mutation document"),envelope=Array.isArray(parsed)?{schema_version:1,mutations:parsed}:parsed;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);for(const key of Object.keys(row))if(!REFERENCED_MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,REFERENCED_MUTATION_ROW_KEYS);return{...row}})}function collectMutationReferences(rows,options){const references=Object.create(null);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),references=collectMutationReferences(rows,options);assertAcyclicCreateReferences(rows);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)),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);return{schema_version:1,mutations,references}}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))appendItemDocumentEntry(documentOptions,key,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{itemDocumentToMutationOptions,parseAtomicMutationControls,parseItemMutationBatch,resolveItemMutationDocument,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]="78df5b0f-51cd-5c24-a1ee-a1d8433be5df")}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};
|
|
2
2
|
//# sourceMappingURL=structured-mutations.js.map
|
|
@@ -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]="
|
|
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 parseJournal(raw,transactionId,stepIds){let parsed;try{parsed=JSON.parse(raw)}catch{throw new TypeError(`Workspace transaction ${transactionId} journal is invalid JSON`)}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw new TypeError(`Workspace transaction ${transactionId} journal is invalid`);const journal=parsed,identityMatches=journal.schemaVersion===1&&journal.transactionId===transactionId,stepsMatch=Array.isArray(journal.stepIds)&&journal.stepIds.length===stepIds.length&&journal.stepIds.every((stepId,index)=>stepId===stepIds[index]),lifecycleMatches=hasValidJournalLifecycle(journal,stepIds),metadataMatches=hasValidJournalMetadata(journal),statusMatches=["applying","compensating","compensated","committed"].includes(String(journal.status));if(!identityMatches||!stepsMatch||!lifecycleMatches||!metadataMatches||!statusMatches)throw new TypeError(`Workspace transaction ${transactionId} journal does not match the supplied plan`);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{shouldCompensate=started.has(step.id)&&(await step.inspect()).state==="applied"}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]="1fae5539-4eb8-5eba-bfd6-a191c89448ec")}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{shouldCompensate=started.has(step.id)&&(await step.inspect()).state==="applied"}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};
|
|
3
3
|
//# sourceMappingURL=workspace-transaction.js.map
|
package/docs/AGENT_GUIDE.md
CHANGED
|
@@ -109,8 +109,9 @@ pm close <item-id> "Acceptance criteria met; verification passed." --resolution
|
|
|
109
109
|
pm release <item-id>
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
If a warning close omitted evidence, use its `
|
|
113
|
-
to append an evidence update while preserving the original close event.
|
|
112
|
+
If a successful warning close omitted evidence, use its `follow_up.suggested_command_args`
|
|
113
|
+
to append an evidence update while preserving the original close event. Failed closes
|
|
114
|
+
retain `recovery.suggested_retry_args`. See
|
|
114
115
|
[Close Evidence Recovery](CLOSE_EVIDENCE_RECOVERY.md).
|
|
115
116
|
|
|
116
117
|
For real subprocess tests, set `cwd`, `PM_PATH`, and `PM_GLOBAL_PATH` to the
|
|
@@ -180,6 +181,10 @@ Over MCP the mutation tools (`pm_create`/`pm_update`/`pm_append`/`pm_close`, and
|
|
|
180
181
|
|
|
181
182
|
`pm create --parent <id>` fails fast when the parent cannot be found. Use `--allow-missing-parent` only for deliberate imports or staged backlog reconstruction.
|
|
182
183
|
|
|
184
|
+
To record many items at once (triage imports, review-thread capture, plan expansion), do not loop `pm create`: write one record per line and commit them in one workspace transaction with `pm item mutate --from items.jsonl --json` (`--dry-run` first). Records use `pm create` field names, `@row-<n>`/`@ref` references link records to each other, and `--upsert-key <field>` turns records that match an existing item into updates. `pm create many` refuses with this route.
|
|
185
|
+
|
|
186
|
+
The default transaction id derives from the exact document bytes, so even a formatting-only edit selects a new id. To replay a committed import, reuse its transaction id and keep the resolved mutation plan unchanged. For formatting-only edits, pass the original id explicitly with `--transaction-id <id>`; replay still requires the same resolved plan. See [Bulk creation](COMMANDS.md#bulk-creation-pm-create-many-route).
|
|
187
|
+
|
|
183
188
|
## Guide Routing for Agents
|
|
184
189
|
|
|
185
190
|
Use the canonical [guide topic map](README.md#guide-topic-map) when local in-CLI documentation routing is useful.
|
|
@@ -187,10 +192,34 @@ Use the canonical [guide topic map](README.md#guide-topic-map) when local in-CLI
|
|
|
187
192
|
## Ownership Rules
|
|
188
193
|
|
|
189
194
|
- Claim before heavy edits.
|
|
190
|
-
- `pm claim <id>`
|
|
191
|
-
-
|
|
195
|
+
- `pm claim <id>` is an atomic test-and-set. A non-terminal item already held by a different principal refuses with `already_claimed_by`; the same principal re-claiming is a no-op.
|
|
196
|
+
- Pass `--if-available` to treat a held item as a skip (`skipped: true`, exit 0) instead of a failure, for example when several agents pull from one queue. The terminal check runs first, so a terminal item still refuses without `--force` even when `--if-available` is set.
|
|
197
|
+
- Use `--force` only for a coordinated takeover of another owner's claim; it is also required to claim terminal items, and to override stale locks when the `governance.force_required_for_stale_lock` policy is enabled (the `minimal` and custom governance presets can disable it). Never force a takeover without explicit approval.
|
|
192
198
|
- Release when pausing, handing off, or after close.
|
|
193
199
|
|
|
200
|
+
### Implementation-to-verification handoff
|
|
201
|
+
|
|
202
|
+
Tracked by [pm-gh1465](../.agents/pm/features/pm-gh1465.toon).
|
|
203
|
+
Projects define their own queues using the existing schema primitives:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pm schema add-status review --role active --description "Implementation complete; awaiting verification"
|
|
207
|
+
pm release <item-id> --handoff review --message "Implemented X; verify with the linked regression"
|
|
208
|
+
pm list --status review
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Handoff requires a claimed, non-terminal item and an evidence message. It changes
|
|
212
|
+
the status and releases ownership in one history event, preserving workflow
|
|
213
|
+
transition and approval policies. It cannot be combined with `--pause` or target
|
|
214
|
+
the configured in-progress or a terminal status. The result includes `handoff`
|
|
215
|
+
with the source status, destination status, and evidence message. No fixed review
|
|
216
|
+
state is added to projects. `pm context` retains the queue's status while counting
|
|
217
|
+
it separately from in-progress work; use `pm list --status review` to select the
|
|
218
|
+
verification queue. A verifier can claim it, close with normal evidence, or use
|
|
219
|
+
`pm claim <item-id> --start` when rework begins. `pm item reopen` remains for
|
|
220
|
+
terminal items. SDK `client.release(id, { handoff: "review", message: "..." })`
|
|
221
|
+
and MCP release options use the same atomic transition.
|
|
222
|
+
|
|
194
223
|
## Reviewed Delivery Closeout
|
|
195
224
|
|
|
196
225
|
Before closing repository defects, set `escape_class` and structured
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Recovering missing close evidence
|
|
2
2
|
|
|
3
|
-
Tracked by [pm-ub1ott](../.agents/pm/issues/pm-ub1ott.toon)
|
|
3
|
+
Tracked by [pm-ub1ott](../.agents/pm/issues/pm-ub1ott.toon) and
|
|
4
|
+
[pm-gh1464](../.agents/pm/issues/pm-gh1464.toon).
|
|
4
5
|
|
|
5
6
|
`pm close --validate-close warn` can close an item while reporting missing
|
|
6
7
|
resolution, expected outcome, or actual outcome. Its SDK result and compact CLI
|
|
7
|
-
output
|
|
8
|
-
`
|
|
8
|
+
output include an optional `follow_up` with `kind: complete_close_evidence`,
|
|
9
|
+
`missing`, `suggested_command`, and `suggested_command_args` when evidence is
|
|
10
|
+
incomplete. The close succeeded; do not retry it. The argument vector selects
|
|
9
11
|
the same tracker and uses `pm update` to add only the missing evidence fields.
|
|
10
|
-
Use `
|
|
12
|
+
Use `follow_up.suggested_command_args` as the authoritative argument vector. Replace
|
|
11
13
|
each `<value>` with the observed outcome before executing it; retain its tracker
|
|
12
14
|
selection and only the flags it supplies for missing fields. Do not add flags
|
|
13
15
|
for evidence already present, because `pm update` would overwrite that evidence.
|
|
@@ -25,11 +27,18 @@ recovery when applicable. `--force` is reserved for an intentional repeated
|
|
|
25
27
|
lifecycle transition. Adding evidence with `pm update` preserves the original
|
|
26
28
|
close timestamp, close reason, and immutable history event, then appends a
|
|
27
29
|
normal update event. It does not retroactively claim that evidence existed at
|
|
28
|
-
the original close.
|
|
30
|
+
the original close. Failed terminal retries retain `recovery.suggested_retry_args`;
|
|
31
|
+
successful closes never emit failure recovery. Complete closes have no follow-up.
|
|
29
32
|
|
|
30
33
|
The update flags for the other missing fields are `--resolution` and
|
|
31
34
|
`--expected-result`; include them only when the generated recovery requests them.
|
|
32
35
|
|
|
33
36
|
For new work, include all three fields in the original close command so its
|
|
34
|
-
immutable event contains the full evidence
|
|
35
|
-
|
|
37
|
+
immutable event contains the full evidence:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pm close <item-id> "Delivered and verified" --resolution "What changed" \
|
|
41
|
+
--expected "Required outcome" --actual "Observed outcome" --validate-close warn
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Warning-mode follow-up exists for incomplete historical or interrupted workflows.
|
package/docs/COMMANDS.md
CHANGED
|
@@ -620,14 +620,19 @@ row summaries. See tracker item [pm-awe3t6](../.agents/pm/issues/pm-awe3t6.toon)
|
|
|
620
620
|
Tracked by [pm-o8z748](../.agents/pm/issues/pm-o8z748.toon) and
|
|
621
621
|
[pm-cyn0y6](../.agents/pm/issues/pm-cyn0y6.toon).
|
|
622
622
|
|
|
623
|
-
`pm item mutate` is the noun-first CLI adapter over the public SDK
|
|
624
|
-
`commitItemMutations` primitive. Pipe either
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
623
|
+
`pm item mutate` is the noun-first CLI adapter over the public SDK planner
|
|
624
|
+
(`planItemMutationDocument`) and `commitItemMutations` primitive. Pipe either
|
|
625
|
+
the legacy non-empty JSON array, a versioned
|
|
626
|
+
`{ "schema_version": 1, "mutations": [...] }` document, or JSON Lines (one
|
|
627
|
+
record per line), or read any of them from a file with `--from <path>`. Mix
|
|
628
|
+
create/update/close/release operations in order. Create rows may declare a
|
|
629
|
+
unique `ref` and omit `id`; exact `@ref` values work in target ids, `parent`,
|
|
630
|
+
`blockedBy`, and dependency `id` fields. The resolver derives replay-stable ids
|
|
631
|
+
before the writer lock and returns a `references` receipt plus an input-ordered
|
|
632
|
+
`rows` receipt (`index`, `ref`, `id`, `op`, `upserted`). When `--transaction-id`
|
|
633
|
+
is omitted the id is derived from the exact document bytes
|
|
634
|
+
(`transaction_id_source: "derived"`), so identical bytes select the same
|
|
635
|
+
journal. Replaying a committed import also requires an unchanged resolved plan:
|
|
631
636
|
|
|
632
637
|
```bash
|
|
633
638
|
pm item mutate \
|
|
@@ -644,13 +649,83 @@ pm item mutate \
|
|
|
644
649
|
JSON
|
|
645
650
|
```
|
|
646
651
|
|
|
652
|
+
`--from <path>` selects a file and rejects an explicitly supplied `--stdin-json`.
|
|
653
|
+
Default stdin input, explicit `--stdin-json` without a file, and `--from -`
|
|
654
|
+
remain supported.
|
|
655
|
+
|
|
647
656
|
Use `--dry-run` to validate and preview without acquiring a writer lock. Reuse
|
|
648
657
|
the exact transaction id and deterministic batch after interruption: the SDK
|
|
649
658
|
journal resumes pending steps and never duplicates a successfully applied
|
|
650
|
-
step.
|
|
651
|
-
|
|
659
|
+
step. A different plan under a committed or resumable journal refuses with
|
|
660
|
+
`transaction_plan_mismatch`; pass a new `--transaction-id` to commit that changed
|
|
661
|
+
plan as its own transaction. A fully compensated attempt has nothing left to
|
|
662
|
+
resume and may accept a new plan under the same transaction id. Formatting-only
|
|
663
|
+
edits change a derived id; pass the original id explicitly to replay them when
|
|
664
|
+
the resolved plan is unchanged. `--create-compensation close|delete`,
|
|
665
|
+
`--lock-ttl-seconds`, and `--lock-wait-ms` expose the transaction safety controls. The equivalent MCP
|
|
652
666
|
surface is `pm_mutate`.
|
|
653
667
|
|
|
668
|
+
Raw mutation `options` use command option types: write `estimatedMinutes: "45"`.
|
|
669
|
+
Item documents may instead use numeric `estimated_minutes: 45`. A non-string
|
|
670
|
+
raw estimate refuses before journal or item writes with
|
|
671
|
+
`mutation_option_value_invalid` (exit 2), identifying the row and option.
|
|
672
|
+
Corrupt durable journals, including a wrong stored schema version or transaction
|
|
673
|
+
identity, refuse with `transaction_journal_invalid`; preserve
|
|
674
|
+
the journal and inspect or restore verified state before retrying. Changing the
|
|
675
|
+
transaction id does not safely recover unknown previously applied work.
|
|
676
|
+
|
|
677
|
+
#### Bulk creation (`pm create many` route)
|
|
678
|
+
|
|
679
|
+
Tracked by [pm-gh1456](../.agents/pm/features/pm-gh1456.toon). `pm create many`
|
|
680
|
+
is not a command; it refuses with `bulk_create_route` and points here. A record
|
|
681
|
+
without `op` is a create document that uses the same field names as
|
|
682
|
+
`pm create` flags or their JSON equivalents (`title`, `type`, `description`,
|
|
683
|
+
`body`, `tags`, `parent`, `priority`, declared custom fields, ...); a create
|
|
684
|
+
row may also carry that document under `item` instead of `options`. Records
|
|
685
|
+
without `ref` or `id` receive `row-<n>` aliases so the receipt and `@row-<n>`
|
|
686
|
+
references stay addressable. One process, one workspace transaction: every
|
|
687
|
+
record is created or none.
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
cat > items.jsonl <<'JSONL'
|
|
691
|
+
{"title":"Imported A","type":"Task","external_id":"ext-1","tags":["import"]}
|
|
692
|
+
{"ref":"b","title":"Imported B","type":"Feature","external_id":"ext-2","parent":"@row-1"}
|
|
693
|
+
JSONL
|
|
694
|
+
pm item mutate --from items.jsonl --dry-run --json # validate, derived transaction id
|
|
695
|
+
pm item mutate --from items.jsonl --json # commit; re-running replays (recovered: true)
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
`--upsert-key <field>` makes a re-run safe for changed data: a create record
|
|
699
|
+
whose field value matches exactly one existing item (core field such as
|
|
700
|
+
`title`, or a declared custom field such as `external_id`) becomes an update
|
|
701
|
+
of that item and is reported with `upserted: true`. Matching and lock-held
|
|
702
|
+
guards use the field's stored metadata key; core option names such as
|
|
703
|
+
`acceptanceCriteria`, declared field keys and their SDK option targets
|
|
704
|
+
(for example `externalId`) resolve to that
|
|
705
|
+
same stored key. The record keeps its
|
|
706
|
+
`ref` in the receipt, and later records may still address it as `@ref`, which
|
|
707
|
+
then resolves to the matched item. A record missing the key, two records
|
|
708
|
+
sharing a key value, or a key value matching several existing items refuses
|
|
709
|
+
before any write (`upsert_key_missing`, `upsert_key_duplicate`,
|
|
710
|
+
`upsert_key_ambiguous`). Matching happens before the writer lock, so every
|
|
711
|
+
record that matched nothing carries its key into the commit, which re-checks
|
|
712
|
+
it under the lock: when another writer created an item with that key value
|
|
713
|
+
after planning, the transaction refuses with `upsert_key_conflict` before any
|
|
714
|
+
write, and a re-run plans the record as an update of that item. A record that
|
|
715
|
+
did match is committed as an update that re-reads the key under the lock and
|
|
716
|
+
refuses with `upsert_match_changed` when the key was changed or removed after
|
|
717
|
+
planning. Either refusal rolls the attempt back completely, and a rolled-back
|
|
718
|
+
attempt leaves its derived transaction id reusable, so re-running the same
|
|
719
|
+
document simply starts a fresh attempt from the current key values.
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
printf '%s\n' '{"title":"Imported A (renamed)","type":"Task","external_id":"ext-1"}' \
|
|
723
|
+
| pm item mutate --upsert-key external_id --json
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Over MCP, `pm_mutate` accepts the same document rows as `{ref?, item: {...}}`
|
|
727
|
+
entries, `upsertKey`, and an optional `transactionId`.
|
|
728
|
+
|
|
654
729
|
`pm item complete` composes evidence, governed closure, and claim release into
|
|
655
730
|
one compensating SDK transaction. It accepts the normal repeatable evidence
|
|
656
731
|
flags and can preview the exact ordered mutations before writing:
|
|
@@ -27,12 +27,16 @@ it is not silently equated with the earlier item snapshot. History-drift
|
|
|
27
27
|
validation passed across all 2,826 current items. The public SDK independently
|
|
28
28
|
verified hash chains for all 2,841 collected streams with zero failures.
|
|
29
29
|
|
|
30
|
-
[The per-item census](ecosystem-review-2026-09-28.json) records every snapshot
|
|
30
|
+
[The per-item census](https://github.com/unbraind/pm-cli/blob/main/docs/ecosystem-review-2026-09-28.json) records every snapshot
|
|
31
31
|
ID, status, content fingerprint, structural classification, missing planning
|
|
32
32
|
fields, historical evidence gaps, and remediation owner. These are mechanical
|
|
33
33
|
findings, not invented historical estimates, outcomes, or review approvals.
|
|
34
34
|
The full raw corpus stays outside public documentation.
|
|
35
35
|
|
|
36
|
+
The census is intentionally excluded from npm. This packaged page uses its
|
|
37
|
+
canonical GitHub file URL under the [documentation policy](../AGENTS.md#documentation-policy),
|
|
38
|
+
so installed readers can access the data without a broken package-relative path.
|
|
39
|
+
|
|
36
40
|
| Measure | Initial observation | Interpretation |
|
|
37
41
|
| --- | ---: | --- |
|
|
38
42
|
| Directed graph edges | 14,097 | Deduplicated directed basis |
|
package/docs/EXTENSIONS.md
CHANGED
|
@@ -305,7 +305,7 @@ Use the public SDK barrel. Do not deep-import from `src/core` or `dist/core`.
|
|
|
305
305
|
|
|
306
306
|
- `api.extension` is a read-only identity (`name`, `layer`, `version`, `capabilities`, `pm_min_version?`, `pm_max_version?`, `source_package?`) for self-identifying logs and version gating without re-reading the manifest.
|
|
307
307
|
- `api.registerCommand(definition)` adds package-owned commands; `read_output` opts readers into the [SDK output contract](SDK.md#minimal-command-extension).
|
|
308
|
-
- `api.registerFlags(command, flags)` adds runtime command flags. A flag may declare `value_type` (canonical; the legacy `type` alias is honored only when `value_type` is absent), `list: true` to accumulate repeated/comma-joined values like core `--tags`,
|
|
308
|
+
- `api.registerFlags(command, flags)` adds runtime command flags. A flag may declare `value_type` (canonical; the legacy `type` alias is honored only when `value_type` is absent), `list: true` to accumulate repeated/comma-joined values like core `--tags`, a `default` applied when the flag is omitted, and `value_validation: "strict"` to refuse malformed `number`/`boolean` tokens before the handler runs (the default `"coerce"` forwards the raw token). See [Flag Contracts](SDK.md#flag-contracts).
|
|
309
309
|
- `api.registerItemFields(fields)` adds custom metadata fields. Agents can set declared fields with repeatable `pm create --field name=value` and `pm update <id> --field name=value`; undeclared names are rejected. `SchemaFieldDefinition.type` is the same closed union used by `pm schema add-field`: `string | number | boolean | string_array | array | object`. Unsupported values fail at compile time for TypeScript authors and at activation for JavaScript packages, with a did-you-mean hint on typos. `string_array` is the repeatable string collection; `array` and `object` require JSON containers.
|
|
310
310
|
- Custom fields whose camel-case option name is already owned by a canonical MCP input remain usable through `options.<name>`, but authoring surfaces report the collision instead of allowing silent top-level shadowing. `pm schema add-field`, `pm profile lint`, and `pm package doctor` name the field, canonical `mcp_tool_input` owner, and nested recovery path. SDK authors can preflight the same rule with `resolvePmToolCustomFieldCollision`.
|
|
311
311
|
- `api.registerItemTypes(types)` adds custom item types.
|
package/docs/SDK.md
CHANGED
|
@@ -483,6 +483,12 @@ not renderer-specific code, so package authors can reuse
|
|
|
483
483
|
`resolvePmCommandOutputBudget`, and
|
|
484
484
|
`estimatePmOutputTokens` in custom transports.
|
|
485
485
|
|
|
486
|
+
Read-output, context-intent, recurrence and diagnostic string compaction retain
|
|
487
|
+
Unicode-safe prefixes. A cutoff through a surrogate pair backs up one UTF-16
|
|
488
|
+
code unit before adding its suffix. Rendering and token receipts use the actual
|
|
489
|
+
retained text; unbounded reads and preserved-string exceptions are unchanged.
|
|
490
|
+
Tracked by [pm-gh1478](../.agents/pm/issues/pm-gh1478.toon).
|
|
491
|
+
|
|
486
492
|
```ts
|
|
487
493
|
import {
|
|
488
494
|
definePmCommandOutputBudget,
|
|
@@ -946,6 +952,27 @@ recurrence reason, accepts only the workspace open or in-progress target, and
|
|
|
946
952
|
returns the terminal evidence captured atomically before the update path clears
|
|
947
953
|
it from active metadata.
|
|
948
954
|
|
|
955
|
+
Lifecycle handoff and receipt guidance are tracked by
|
|
956
|
+
[pm-gh1465](../.agents/pm/features/pm-gh1465.toon) and
|
|
957
|
+
[pm-gh1464](../.agents/pm/issues/pm-gh1464.toon). Register a project-specific
|
|
958
|
+
queue with `client.schemaAddStatus("review", { role: ["active"] })`, then call
|
|
959
|
+
`client.release(id, { handoff: "review", message: "Implemented; run linked verification" })`.
|
|
960
|
+
The SDK atomically changes status and clears ownership in one release history
|
|
961
|
+
event, applying project ownership, type-transition, and workflow policies.
|
|
962
|
+
`ReleaseMutationOptions` declares the handoff option; `ReleaseResult.handoff`
|
|
963
|
+
identifies the source and destination states and evidence. `client.list({ status:
|
|
964
|
+
"review" })` selects the verifier queue; context preserves its status without
|
|
965
|
+
counting it as in-progress work. The destination must already exist, be
|
|
966
|
+
non-terminal, and differ from the configured in-progress status. Handoff needs
|
|
967
|
+
a claimed source and evidence message, and cannot be combined with pause.
|
|
968
|
+
|
|
969
|
+
A successful warning-mode close with missing evidence returns optional
|
|
970
|
+
`follow_up: CloseEvidenceFollowUp`. Its `kind` is `complete_close_evidence` and
|
|
971
|
+
`suggested_command_args` selects an evidence-only update of the same item and
|
|
972
|
+
tracker. Consumers migrating from successful-close `recovery` should read
|
|
973
|
+
`follow_up` instead; a successful close never asks for a retry. Failed close
|
|
974
|
+
commands retain failure recovery. See [close evidence guidance](CLOSE_EVIDENCE_RECOVERY.md).
|
|
975
|
+
|
|
949
976
|
### Atomic workspace transactions
|
|
950
977
|
|
|
951
978
|
Tracked by [pm-4e12](../.agents/pm/features/pm-4e12.toon), with the VCS
|
|
@@ -1103,7 +1130,14 @@ bulk sync), `commitItemMutations` wraps the coordinator so callers describe
|
|
|
1103
1130
|
the mutations instead of hand-writing a step array. The helper wires the
|
|
1104
1131
|
crash-consistency contract for you: creates use their resolved stable `id` as
|
|
1105
1132
|
the idempotency key (exists-by-id inspection) and are compensated by closing
|
|
1106
|
-
the item (or deleting it with `createCompensation: "delete"`)
|
|
1133
|
+
the item (or deleting it with `createCompensation: "delete"`). A retry refuses
|
|
1134
|
+
with `transaction_create_compensated` while a creation's latest closure belongs
|
|
1135
|
+
to compensation, even if its close prose was edited. Review its immutable history
|
|
1136
|
+
and explicitly reopen it before retrying; the SDK preserves later lifecycle
|
|
1137
|
+
decisions instead of silently reopening the item or reporting it committed.
|
|
1138
|
+
A later explicit user close or status update takes precedence over the earlier
|
|
1139
|
+
compensation close and is preserved when the transaction resumes.
|
|
1140
|
+
Updates stamp a
|
|
1107
1141
|
durable history marker for applied-detection and are compensated by restoring
|
|
1108
1142
|
the captured pre-mutation version; closes treat an already-terminal target as
|
|
1109
1143
|
applied and are likewise compensated by version restore; releases restore the
|
|
@@ -1241,6 +1275,70 @@ await commitItemMutations({
|
|
|
1241
1275
|
});
|
|
1242
1276
|
```
|
|
1243
1277
|
|
|
1278
|
+
For imports, use `planItemMutationDocument` (tracked by
|
|
1279
|
+
[pm-gh1456](../.agents/pm/features/pm-gh1456.toon)). It reads the workspace
|
|
1280
|
+
schema so declared custom fields route through their option targets, accepts
|
|
1281
|
+
JSON Lines and op-less create documents (records shaped like `pm create
|
|
1282
|
+
--stdin-json` input, optionally wrapped as `{ ref, item }`), synthesizes
|
|
1283
|
+
`row-<n>` aliases for records without `ref`/`id`, derives a stable
|
|
1284
|
+
`transaction_id` from the document bytes when none is supplied, and resolves
|
|
1285
|
+
`upsertKey` against existing items so matching create records become updates;
|
|
1286
|
+
core camelCase option names (such as `acceptanceCriteria`) and declared field
|
|
1287
|
+
keys or option targets resolve to the stored metadata key before candidate
|
|
1288
|
+
reads and lock-held guards.
|
|
1289
|
+
An explicitly supplied blank `upsertKey` is a usage error; omit it to select plain creation.
|
|
1290
|
+
Unrelated extension metadata keys retain their exact spelling, including camelCase.
|
|
1291
|
+
Item documents also accept known core camelCase option names, including inside
|
|
1292
|
+
`{ item }` wrappers. Exact stored metadata keys win when both spellings appear.
|
|
1293
|
+
Records that match nothing carry a `unique` guard that `commitItemMutations`
|
|
1294
|
+
re-checks under the writer lock and refuses with `upsert_key_conflict` when a
|
|
1295
|
+
concurrent writer created the key value after planning; matched records become
|
|
1296
|
+
updates carrying a `match` guard checked against the locked target document,
|
|
1297
|
+
after acquiring its item lock. It refuses with `upsert_match_changed` when the
|
|
1298
|
+
target's key changed in between, including changes made by ordinary item updates.
|
|
1299
|
+
Successful update, close and release steps invalidate cached unique-key scans
|
|
1300
|
+
inside a mixed batch, so subsequent creates see the current persisted values,
|
|
1301
|
+
including changes to earlier creates in that batch.
|
|
1302
|
+
Every create also advances cached scans from its returned metadata, including
|
|
1303
|
+
unguarded creates and records guarded on a different key, without a new project scan.
|
|
1304
|
+
The returned `rows` receipt maps every input record, in order, to its resolved
|
|
1305
|
+
`id` and `op`:
|
|
1306
|
+
|
|
1307
|
+
```ts
|
|
1308
|
+
import {
|
|
1309
|
+
commitItemMutations,
|
|
1310
|
+
planItemMutationDocument,
|
|
1311
|
+
} from "@unbrained/pm-cli/sdk";
|
|
1312
|
+
|
|
1313
|
+
const plan = await planItemMutationDocument({
|
|
1314
|
+
pmRoot,
|
|
1315
|
+
input: [
|
|
1316
|
+
'{"title":"Imported A","type":"Task","external_id":"ext-1"}',
|
|
1317
|
+
'{"ref":"b","title":"Imported B","type":"Feature","parent":"@row-1","external_id":"ext-2"}',
|
|
1318
|
+
].join("\n"),
|
|
1319
|
+
upsertKey: "external_id",
|
|
1320
|
+
});
|
|
1321
|
+
const result = await commitItemMutations({
|
|
1322
|
+
pmRoot,
|
|
1323
|
+
author: "import-agent",
|
|
1324
|
+
transactionId: plan.transaction_id,
|
|
1325
|
+
mutations: plan.mutations,
|
|
1326
|
+
});
|
|
1327
|
+
console.log(plan.transaction_id_source, plan.rows, result.recovered);
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
After a successful commit, reuse its transaction id and unchanged resolved
|
|
1331
|
+
mutation plan to replay it (`recovered: true`). A formatting-only edit changes
|
|
1332
|
+
the derived id; pass the original id explicitly to replay an unchanged plan.
|
|
1333
|
+
The planner excludes items created by that transaction from upsert matching,
|
|
1334
|
+
retaining those create rows.
|
|
1335
|
+
If document edits, external matches, or schema context change the resolved
|
|
1336
|
+
plan under that committed id, the coordinator refuses with
|
|
1337
|
+
`transaction_plan_mismatch`; use a new transaction id for the changed import.
|
|
1338
|
+
Corrupt journal JSON, stored schema version/transaction identity, lifecycle state or metadata instead refuses with
|
|
1339
|
+
`transaction_journal_invalid`. Preserve and inspect or restore the journal;
|
|
1340
|
+
starting a new id can duplicate work whose durable state cannot be verified.
|
|
1341
|
+
|
|
1244
1342
|
`buildItemCompletionMutations` creates the ordered evidence/update, close, and
|
|
1245
1343
|
release plan for inspection. `commitItemCompletion` executes that plan through
|
|
1246
1344
|
the same durable coordinator, restoring annotations, linked artifacts,
|
|
@@ -2225,6 +2323,20 @@ same list/default semantics as core flags:
|
|
|
2225
2323
|
`default: "a,b"` or `default: ["a,b", "c"]`) are split into elements. A default
|
|
2226
2324
|
that would not cleanly coerce under the declared `value_type` (e.g.
|
|
2227
2325
|
`value_type: "number", default: "abc"`) is rejected at registration.
|
|
2326
|
+
- `value_validation` (`"coerce"` | `"strict"`, tracked by
|
|
2327
|
+
[pm-extension-scalar-validation](../.agents/pm/features/pm-extension-scalar-validation.toon))
|
|
2328
|
+
decides what happens to a `number`/`boolean` token that does not coerce.
|
|
2329
|
+
`coerce` is the compatible default: the raw token reaches the handler for its
|
|
2330
|
+
own validation (`--limit abc` arrives as `"abc"`). `strict` refuses the
|
|
2331
|
+
invocation before dispatch with `extension_flag_value_invalid` (exit 2),
|
|
2332
|
+
naming the flag, the command, the expected grammar, and the received token;
|
|
2333
|
+
list entries are checked one by one, including attached values on boolean
|
|
2334
|
+
list switches. CSV boolean lists accept `true,false`; repeatable boolean
|
|
2335
|
+
lists keep each occurrence as one value. Switch presence (`--verbose`,
|
|
2336
|
+
`--no-verbose`), the legacy `bool` optional-value grammar (`--enabled` alone
|
|
2337
|
+
is `true`), declared defaults, and string flags are unaffected. `strict` on a
|
|
2338
|
+
`string` or untyped flag is rejected at registration because there is nothing
|
|
2339
|
+
to validate.
|
|
2228
2340
|
|
|
2229
2341
|
```ts
|
|
2230
2342
|
api.registerFlags("report", [
|
|
@@ -2235,7 +2347,7 @@ api.registerFlags("report", [
|
|
|
2235
2347
|
list: true,
|
|
2236
2348
|
default: "all",
|
|
2237
2349
|
},
|
|
2238
|
-
{ long: "--limit", value_type: "number", default: 20 },
|
|
2350
|
+
{ long: "--limit", value_type: "number", default: 20, value_validation: "strict" },
|
|
2239
2351
|
]);
|
|
2240
2352
|
```
|
|
2241
2353
|
|
package/docs/TESTING.md
CHANGED
|
@@ -650,6 +650,21 @@ pm test <item-id> --run --workspace-context snapshot --override-linked-workspace
|
|
|
650
650
|
The real filesystem and CLI snapshot suite runs in the required Node 22
|
|
651
651
|
telemetry regression job and native Windows regression job before merge.
|
|
652
652
|
|
|
653
|
+
Windows execution is tracked by [pm-bj7rq0](../.agents/pm/issues/pm-bj7rq0.toon).
|
|
654
|
+
Script and native keeper cohorts run independently on Windows Node 24. Their
|
|
655
|
+
source/script keeper steps retain the two-worker cap, and each cohort retains
|
|
656
|
+
the 12-minute deadline. The required
|
|
657
|
+
`Windows regression (Node 24)` check waits for both cohorts and fails if either
|
|
658
|
+
fails, is cancelled, or is skipped. This preserves every selected regression
|
|
659
|
+
while avoiding a shared serial job deadline. See GitHub's
|
|
660
|
+
[job dependency semantics](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-jobs#defining-prerequisite-jobs).
|
|
661
|
+
|
|
662
|
+
The real point-read corpus keeper runs with the lifecycle latency keeper in
|
|
663
|
+
the existing sequential performance phase after parallel contracts. Its
|
|
664
|
+
1,500-item corpus, work-observer assertions and 30-second deadline remain
|
|
665
|
+
unchanged. Standalone script identity is resolved from the test module URL,
|
|
666
|
+
independently of temporary SDK working directories.
|
|
667
|
+
|
|
653
668
|
The real-filesystem regression for this contract is tracked by
|
|
654
669
|
[pm-5iwfkj](../.agents/pm/issues/pm-5iwfkj.toon).
|
|
655
670
|
|