@unbrained/pm-cli 2026.10.19 → 2026.10.20
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +33 -0
- package/dist/cli/error-guidance.d.ts +7 -0
- package/dist/cli/error-guidance.js +2 -2
- package/dist/cli/register-structured-mutation.js +1 -1
- package/dist/cli-bundle/bundle-manifest.json +1 -1
- package/dist/cli-bundle/chunks/{chunk-QBNEZII5.js → chunk-2GGGOD7P.js} +13 -13
- package/dist/cli-bundle/chunks/{chunk-4CXQSRSP.js → chunk-7NHA5K77.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-NX7Q4LGN.js → chunk-7ZOIZ2AZ.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-6DS5CAN2.js → chunk-GHJOMRSF.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-O2ZRRLAP.js → chunk-KU4Q3MSI.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-DMFM5R4G.js → chunk-LS55VKRK.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-QG4LMU7A.js → chunk-PNPE2OVJ.js} +43 -43
- package/dist/cli-bundle/chunks/chunk-PTL3TBT6.js +20 -0
- package/dist/cli-bundle/chunks/{chunk-LBEMB3BW.js → chunk-QETMESEB.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-4MJ6WBMA.js → chunk-QMZ73FCW.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-QQYCB2LO.js +2 -0
- package/dist/cli-bundle/chunks/chunk-TZU6U5ZE.js +3 -0
- package/dist/cli-bundle/chunks/{chunk-H3XYP24A.js → chunk-VLJLWZBO.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-XAWE4EAK.js +2 -0
- package/dist/cli-bundle/chunks/chunk-YECCKB4E.js +3 -0
- package/dist/cli-bundle/chunks/{chunk-5GZSX5HU.js → chunk-YT2Z3JPC.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-4XFVAXA4.js → chunk-YTHI5DTH.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-D3EDNAN7.js → chunk-YW6SIKYP.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-XH7C3A5N.js → chunk-ZA3XXHEE.js} +6 -6
- package/dist/cli-bundle/chunks/context-budget-recovery-L2WP5S26.js +2 -0
- package/dist/cli-bundle/{focused-chunks/indexed-receipt-blobs-QW2VWWJE.js → chunks/indexed-receipt-blobs-U7VGKIYC.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-WQUVDJIH.js → register-list-query-NNDV6F6I.js} +3 -3
- package/dist/cli-bundle/chunks/{register-mutation-IWIBQIO3.js → register-mutation-3GCBSNQS.js} +4 -4
- package/dist/cli-bundle/chunks/register-operations-YKDGNKUL.js +2 -0
- package/dist/cli-bundle/chunks/register-setup-AOUZQQWB.js +2 -0
- package/dist/cli-bundle/chunks/{settlement-hook-UNFMCKKQ.js → settlement-hook-CLRGA6O7.js} +2 -2
- package/dist/cli-bundle/chunks/stage-6QXYRMSU.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-WZTEFFQI.js → chunk-37SU7BZI.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-5YTOYBTZ.js → chunk-6IU7BESB.js} +3 -3
- package/dist/cli-bundle/focused-chunks/chunk-AS3MOBO3.js +25 -0
- package/dist/cli-bundle/focused-chunks/{chunk-PXDR77TO.js → chunk-AW35RDKA.js} +22 -22
- package/dist/cli-bundle/focused-chunks/{chunk-RLRMNFLF.js → chunk-E4II2D2B.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-FEN7WOOP.js → chunk-F7FD25TG.js} +12 -12
- package/dist/cli-bundle/focused-chunks/{chunk-JDBO7SMR.js → chunk-GYHZ7W6O.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-EOQ6W25I.js → chunk-H5Q2HUU6.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-3TEIACFU.js → chunk-HUZAXSIJ.js} +3 -3
- package/dist/cli-bundle/focused-chunks/{chunk-JBJJW2WL.js → chunk-J6EYUGAR.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-JEXZEIAX.js +91 -0
- package/dist/cli-bundle/focused-chunks/{chunk-ENZMATOY.js → chunk-KDGUIIRU.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-H6BVQ63U.js → chunk-KKRFSI2N.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-GWTV45SB.js → chunk-LJPUJ2KK.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-MQ42BFGX.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-HYMYAGSY.js → chunk-NI6NS44Q.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-L4PL3GZR.js → chunk-VAYS754X.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-SKYLOPU2.js → chunk-VLSER3XW.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-JCLC6HKT.js → chunk-VS6VTWTZ.js} +3 -3
- package/dist/cli-bundle/focused-chunks/{chunk-OV2WVLUR.js → chunk-ZGEK57KD.js} +2 -2
- package/dist/cli-bundle/focused-chunks/context-budget-recovery-5MKOCGY5.js +2 -0
- package/dist/cli-bundle/{chunks/indexed-receipt-blobs-BVSHK4WQ.js → focused-chunks/indexed-receipt-blobs-H2G6CCGO.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{settlement-hook-X4FNHTOD.js → settlement-hook-5SNBBT7C.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{stage-MZSVQYO4.js → stage-EMTISXR7.js} +2 -2
- package/dist/cli-bundle/main.js +21 -21
- 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 +21 -21
- 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 +55 -55
- 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 +8 -8
- package/dist/core/fs/fs-utils.d.ts +2 -2
- package/dist/core/fs/fs-utils.js +1 -1
- package/dist/core/history/event-index.js +2 -2
- package/dist/core/output/output.d.ts +7 -2
- package/dist/core/output/output.js +5 -5
- package/dist/core/shared/errors.d.ts +48 -0
- package/dist/core/shared/errors.js +1 -1
- package/dist/core/store/settings.js +2 -2
- package/dist/mcp/server.js +1 -1
- package/dist/sdk/core.d.ts +1 -1
- package/dist/sdk/core.js +1 -1
- package/dist/sdk/dependencies/context-budget-recovery.d.ts +21 -0
- package/dist/sdk/dependencies/context-budget-recovery.js +2 -0
- package/dist/sdk/dependencies.js +1 -1
- package/dist/sdk/errors.d.ts +4 -1
- package/dist/sdk/errors.js +1 -1
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/item-transaction.d.ts +13 -0
- package/dist/sdk/item-transaction.js +1 -1
- package/dist/sdk/lifecycle/structured-mutation-plan.d.ts +14 -1
- package/dist/sdk/lifecycle/structured-mutation-plan.js +1 -1
- package/dist/sdk/output/list-envelope.d.ts +7 -0
- package/dist/sdk/output/list-envelope.js +2 -0
- package/dist/sdk/output.d.ts +1 -0
- package/dist/sdk/output.js +2 -2
- package/dist/sdk/public-surface.json +1 -1
- package/dist/sdk/relationship-context.d.ts +2 -0
- package/dist/sdk/relationship-context.js +1 -1
- package/dist/sdk/structured-mutations.d.ts +2 -0
- package/dist/sdk/structured-mutations.js +1 -1
- package/docs/COMMANDS.md +14 -0
- package/docs/PR_REVIEW_LOOP.md +32 -12
- package/docs/SCRIPTING.md +10 -0
- package/docs/SDK.md +77 -0
- package/docs/TESTING.md +25 -1
- package/marketplace.json +1 -1
- package/package.json +1 -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-4ICKT4GH.js +0 -3
- package/dist/cli-bundle/chunks/chunk-OEJ3G7XT.js +0 -20
- package/dist/cli-bundle/chunks/chunk-QKAAZWGC.js +0 -3
- package/dist/cli-bundle/chunks/chunk-TK3G3GAP.js +0 -2
- package/dist/cli-bundle/chunks/register-operations-SUWOZNDU.js +0 -2
- package/dist/cli-bundle/chunks/register-setup-QGM5YJW7.js +0 -2
- package/dist/cli-bundle/chunks/stage-7DELI4Z5.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-2IEWB6MB.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-BJMPRB7A.js +0 -91
- package/dist/cli-bundle/focused-chunks/chunk-KLJN44VY.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-Y54ONBEX.js +0 -25
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { RelationshipGraph,RelationshipKindRegistry,type RelationshipEdge,type RelationshipTraversalDirection } from "./relationships.js";
|
|
2
|
+
/** Canonical default shared by the kernel and rendered-budget recovery. */
|
|
3
|
+
export declare const DEFAULT_RELATIONSHIP_CONTEXT_TOKEN_BUDGET = 1200;
|
|
2
4
|
/** Semantic family explaining why a node or edge participates in graph context. */
|
|
3
5
|
export type RelationshipContextRole = "prerequisite" | "dependent" | "ancestor" | "descendant" | "provenance" | "related";
|
|
4
6
|
/**
|
|
@@ -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]="
|
|
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]="5a56b848-63e7-5499-bed3-ee2b2d65e200")}catch{}})();import{createQueryFingerprint,encodeQueryCursor,resolveQueryCursorStart}from"./pagination.js";import{createRelationshipKindRegistry}from"./relationships.js";const DEFAULT_RELATIONSHIP_CONTEXT_TOKEN_BUDGET=1200,RELATIONSHIP_CONTEXT_ROLE_PRIORITY=["prerequisite","dependent","ancestor","descendant","provenance","related"],tokenEncoder=new TextEncoder;function positiveInteger(value,fallback,field){const resolved=value??fallback;if(!Number.isInteger(resolved)||resolved<1)throw new TypeError(`Relationship context ${field} must be positive`);return resolved}function estimateTokens(value){return Math.max(1,Math.ceil(tokenEncoder.encode(JSON.stringify(value)).byteLength/4))}function explainDirectEdge(edge,node,registry){const definition=registry.resolve(edge.kind);if(definition?.ordering){const before=(definition.precedence??"source_before_target")==="source_before_target"?edge.source:edge.target;return node===before?"prerequisite":"dependent"}if(definition?.hierarchy){const parent=definition.hierarchyDirection==="source_parent"?edge.source:edge.target;return node===parent?"ancestor":"descendant"}return edge.kind==="discovered_from"||edge.kind==="incident_from"?"provenance":"related"}function directReasons(rootEdges,root,node,registry){const reasons=rootEdges.filter(edge=>edge.source===root&&edge.target===node||edge.target===root&&edge.source===node).map(edge=>explainDirectEdge(edge,node,registry));return[...new Set(reasons)].sort()}function primaryRole(roles){return RELATIONSHIP_CONTEXT_ROLE_PRIORITY.find(role=>roles.includes(role))}function discoverNodes(graph,root,registry,options){const direction=options.direction??"both",maxDepth=options.maxDepth??3;if(!Number.isInteger(maxDepth)||maxDepth<0)throw new TypeError("Relationship context maxDepth must be non-negative");const seen=new Set([root]),queue=[{id:root,distance:0}],rows=[];let inspectedEdges=0,depthTruncated=!1;for(let index=0;index<queue.length;index+=1){options.signal?.throwIfAborted();const current=queue[index];if(current.distance>=maxDepth&&depthTruncated)continue;const adjacent=graph.neighborEdges(current.id,{direction,kinds:options.kinds,signal:options.signal});if(inspectedEdges+=adjacent.meta.inspectedEdges,current.distance>=maxDepth){adjacent.value.some(row=>!seen.has(row.id))&&(depthTruncated=!0);continue}for(const{id,edge}of adjacent.value){if(seen.has(id))continue;seen.add(id);const row={id,distance:current.distance+1,via:current.id,role:explainDirectEdge(edge,id,registry)};rows.push(row),queue.push(row)}}return rows.sort((left,right)=>left.distance-right.distance||left.id.localeCompare(right.id)),{rows,visitedNodes:queue.length,inspectedEdges,depthTruncated}}function summarizeDirectEdges(graph,root,registry,options){const directEdges={prerequisite:0,dependent:0,ancestor:0,descendant:0,provenance:0,related:0},rows=graph.neighborEdges(root,{direction:options.direction??"both",kinds:options.kinds,signal:options.signal}).value;for(const{id,edge}of rows)directEdges[explainDirectEdge(edge,id,registry)]+=1;return{directEdges,directTotal:rows.length,rootEdges:rows.map(({edge})=>edge)}}function selectContextNodes(params){const nodes=[];let usedTokens=params.initialTokens;for(const candidate of params.candidates){if(nodes.length>=params.nodeLimit)break;const direct=candidate.distance===1?directReasons(params.rootEdges,params.rootId,candidate.id,params.registry):[],node={...params.details.get(candidate.id)??{id:candidate.id},distance:candidate.distance,role:direct.length>0?primaryRole(direct):candidate.role,via:candidate.via,reasons:direct.length>0?direct:[`${candidate.role} via ${candidate.via} (depth ${candidate.distance})`]},cost=estimateTokens(node);if(usedTokens+cost>params.tokenBudget)break;nodes.push(node),usedTokens+=cost}if(params.candidates.length>0&&nodes.length===0)throw new TypeError("Relationship context tokenBudget cannot fit one node");return{nodes,usedTokens}}function selectContextEdges(params){const kinds=params.options.kinds?new Set(params.options.kinds.map(kind=>params.registry.require(kind).kind)):void 0,candidates=params.graph.edges().filter(edge=>{if(!params.included.has(edge.source)||!params.included.has(edge.target))return!1;if(kinds===void 0||kinds.has(edge.kind))return!0;const inverse=params.registry.resolve(edge.kind)?.inverse;return inverse!==void 0&&kinds.has(inverse)}),edges=[];let usedTokens=params.initialTokens;for(const edge of candidates){if(edges.length>=params.edgeLimit)break;const cost=estimateTokens(edge);if(usedTokens+cost>params.tokenBudget)break;edges.push(edge),usedTokens+=cost}return{edges,usedTokens,candidateCount:candidates.length}}function buildRelationshipContext(graph,rootId,details,options={}){if(!graph.nodes().includes(rootId))throw new TypeError(`Relationship node not found: ${rootId}`);const nodeLimit=positiveInteger(options.nodeLimit,20,"nodeLimit"),edgeLimit=positiveInteger(options.edgeLimit,40,"edgeLimit"),tokenBudget=positiveInteger(options.tokenBudget,DEFAULT_RELATIONSHIP_CONTEXT_TOKEN_BUDGET,"tokenBudget"),registry=options.registry??createRelationshipKindRegistry(),byId=new Map(details.map(detail=>[detail.id,detail])),rootDetails=byId.get(rootId)??{id:rootId},{evidence:rootEvidence=[],...root}=rootDetails,evidence=[...rootEvidence],discovery=discoverNodes(graph,rootId,registry,options),{rootEdges,...directSummary}=summarizeDirectEdges(graph,rootId,registry,options),fingerprint=createQueryFingerprint("relationship-context",{rootId,direction:options.direction??"both",kinds:[...options.kinds??[]].sort(),maxDepth:options.maxDepth??3}),pageStart=resolveQueryCursorStart(discovery.rows,options.cursor,fingerprint,({id})=>id),candidates=discovery.rows.slice(pageStart),initialTokens=estimateTokens(root)+estimateTokens(evidence);if(initialTokens>tokenBudget)throw new TypeError("Relationship context tokenBudget cannot fit root and evidence");const nodeSelection=selectContextNodes({candidates,details:byId,rootEdges,rootId,registry,nodeLimit,tokenBudget,initialTokens}),{nodes}=nodeSelection,included=new Set([rootId,...nodes.map(({id})=>id)]),edgeSelection=selectContextEdges({graph,included,options,registry,edgeLimit,tokenBudget,initialTokens:nodeSelection.usedTokens}),{edges,usedTokens}=edgeSelection,consumed=pageStart+nodes.length,hasMoreNodes=consumed<discovery.rows.length,lastNode=nodes.at(-1),omittedEdges=edgeSelection.candidateCount-edges.length,truncated=discovery.depthTruncated||hasMoreNodes||omittedEdges>0,summary={rootId,...root.status===void 0?{}:{rootStatus:root.status},...directSummary,discoveredNodes:discovery.rows.length,returnedNodes:nodes.length,returnedEdges:edges.length,omittedNodes:discovery.rows.length-consumed,omittedEdges,evidenceCount:evidence.length,hasMore:hasMoreNodes};return{root,summary,nodes,edges,evidence,meta:{exact:!0,truncated,completeness:truncated?"truncated":"complete",nodeLimit,edgeLimit,tokenBudget,usedTokens,visitedNodes:discovery.visitedNodes,inspectedEdges:discovery.inspectedEdges,omittedNodes:discovery.rows.length-consumed,omittedEdges,...hasMoreNodes&&lastNode?{nextCursor:encodeQueryCursor(fingerprint,lastNode.id,consumed-1)}:{}}}}export{DEFAULT_RELATIONSHIP_CONTEXT_TOKEN_BUDGET,buildRelationshipContext};
|
|
2
2
|
//# sourceMappingURL=relationship-context.js.map
|
|
@@ -42,6 +42,8 @@ id:string;
|
|
|
42
42
|
op:BulkItemMutation["op"];
|
|
43
43
|
/** Whether keyed upsert turned a create row into an update of an existing item. */
|
|
44
44
|
upserted?:true;
|
|
45
|
+
/** Lifecycle warnings for this row when returned by a staged document preview. */
|
|
46
|
+
warnings?:readonly string[];
|
|
45
47
|
}
|
|
46
48
|
/** Fully validated mutation document ready for preview or commit. */
|
|
47
49
|
export interface ResolvedItemMutationDocument {
|
|
@@ -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]="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};
|
|
1
|
+
(function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7b430d12-7a5b-52c2-9a6b-e4c33a39da76")}catch{}})();import crypto from"node:crypto";import{ITEM_MUTATION_FACET_OPTION_MAPPINGS}from"./lifecycle/mutation-facet-contracts.js";import{normalizeItemId,normalizePrefix}from"../core/item/id.js";import{preserveMutationStdinTokenLiterals}from"../core/item/parse.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{levenshteinDistanceWithinLimit}from"../core/shared/levenshtein.js";import{describeUnknownError}from"./error-runtime.js";import{CLOSE_FLAG_CONTRACTS,RELEASE_FLAG_CONTRACTS}from"./cli-contracts/flag-contracts.js";import{CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS,UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS}from"./cli-contracts/commander-mutation-options.js";import{runtimeFieldOptionTarget}from"../core/schema/runtime-schema.js";import{CORE_ITEM_FIELD_KEYS as ITEM_FIELD_KEYS,resolveCoreUpsertMetadataKey}from"./lifecycle/core-item-fields.js";import{readRuntimeFieldOptionValue}from"../core/schema/runtime-field-values.js";import{assertMutationOptionValueTypes}from"./lifecycle/mutation-option-validation.js";const MUTATION_ROW_KEYS=["op","id","reason","options","unique","match"],REFERENCED_MUTATION_ROW_KEYS=[...MUTATION_ROW_KEYS,"ref"],DOCUMENT_MUTATION_ROW_KEYS=[...REFERENCED_MUTATION_ROW_KEYS,"item"],DERIVED_TRANSACTION_ID_PREFIX="mutate-",SYNTHESIZED_REFERENCE_PREFIX="row-",MUTATION_DOCUMENT_KEYS=["schema_version","mutations"],MUTATION_REFERENCE_SOURCE="[a-z][a-z0-9._-]{0,63}",MUTATION_REFERENCE_PATTERN=new RegExp(`^${MUTATION_REFERENCE_SOURCE}$`,"u"),MUTATION_DEPENDENCY_REFERENCE_PATTERN=new RegExp(`(^|,)id=@(${MUTATION_REFERENCE_SOURCE})(?=,|$)`,"gu"),flagToOptionKey=flag=>flag.slice(2).replaceAll(/-([a-z])/gu,(_match,letter)=>letter.toUpperCase()),ITEM_ENVELOPE_KEYS=["item","linked","schedule","claim_state","children","omission_receipt","row_contract","context_intent"],READ_ONLY_ITEM_KEYS=new Set(["created_at","updated_at","closed_at","version","format_version","path","author","collection_counts","notes_count","tests_count"]),UPDATE_READ_ONLY_ITEM_KEYS=new Set([...READ_ONLY_ITEM_KEYS,"completed_at","id"]),MUTATION_OPTION_KEYS={create:CREATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),update:UPDATE_COMMANDER_OPTION_REGISTRATION_CONTRACTS.map(contract=>contract.target),close:CLOSE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag)).filter(key=>key!=="reason"),release:RELEASE_FLAG_CONTRACTS.map(contract=>flagToOptionKey(contract.flag))};function deriveMutationTransactionId(input){const digest=crypto.createHash("sha256").update(input).digest("hex");return`${DERIVED_TRANSACTION_ID_PREFIX}${digest.slice(0,24)}`}function parseAtomicMutationControls(value){const controls={},compensation=value.createCompensation;if(compensation!==void 0){if(compensation!=="close"&&compensation!=="delete")throw new PmCliError("createCompensation must be close or delete.",EXIT_CODE.USAGE);controls.createCompensation=compensation}for(const key of["lockTtlSeconds","lockWaitMs"]){const raw=value[key];if(raw===void 0)continue;const parsed=typeof raw=="number"||typeof raw=="string"&&raw.trim().length>0?Number(raw):Number.NaN;if(!Number.isSafeInteger(parsed)||parsed<=0)throw new PmCliError(`${key} must be a positive safe integer.`,EXIT_CODE.USAGE);controls[key]=parsed}return controls}function isPlainObject(value){if(typeof value!="object"||value===null||Array.isArray(value))return!1;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function nearestKey(key,candidates){const limit=Math.max(1,Math.min(3,Math.floor(key.length/4)+1));let best;for(const candidate of candidates){const distance=levenshteinDistanceWithinLimit(key,candidate,limit);distance!==null&&(best===void 0||distance<best.distance)&&(best={key:candidate,distance})}return best?.key}function unknownKeyError(scope,key,candidates){const suggestion=nearestKey(key,candidates);return new PmCliError(suggestion===void 0?`${scope} does not recognize key "${key}". Allowed keys: ${candidates.join(", ")}.`:`${scope} does not recognize key "${key}". Did you mean "${suggestion}"?`,EXIT_CODE.USAGE,{code:"stdin_json_unknown_key",required:`Use documented ${scope} keys only.`,why:"Strict structured input prevents silent typos from creating incomplete or unintended mutations.",nextSteps:[suggestion===void 0?`Remove or correct "${key}".`:`Replace "${key}" with "${suggestion}".`]})}function parseJsonValue(input,label){try{return JSON.parse(input)}catch(error){const detail=error instanceof Error?error.message:String(error);throw new PmCliError(`${label} must be valid JSON: ${detail}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:`Pipe one valid JSON ${label.toLowerCase()} into stdin.`,why:"Structured mutation input must parse deterministically before any tracker write begins."})}}function validateMutationObject(value,index){if(!isPlainObject(value))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);for(const key of Object.keys(value))if(!MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,MUTATION_ROW_KEYS);return value}function validateMutationOptions(options,op,index,runtimeFields){if(options===void 0)return;if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} options must be an object.`,EXIT_CODE.USAGE);const allowed=[...MUTATION_OPTION_KEYS[op],...runtimeFieldOptionKeys(op,runtimeFields)];for(const key of Object.keys(options))if(!allowed.includes(key))throw unknownKeyError(`mutation ${index+1} ${op} options`,key,allowed);return assertMutationOptionValueTypes(options,op,index),options}function validateMutationKeyGuard(value,op,index,field){if(value===void 0)return;const allowedOp=field==="unique"?"create":"update";if(op!==allowedOp)throw new PmCliError(`Mutation ${index+1} ${field} is only valid for ${allowedOp} operations.`,EXIT_CODE.USAGE);const key=isPlainObject(value)&&typeof value.key=="string"?value.key.trim():"";if(!isPlainObject(value)||key.length===0||value.value===void 0||value.value===null)throw new PmCliError(`Mutation ${index+1} ${field} must be an object with a non-empty key and a value.`,EXIT_CODE.USAGE);return{key,value:value.value}}function validateMutationRow(value,index,runtimeFields){const mutation=validateMutationObject(value,index),{op,id,reason}=mutation;if(op!=="create"&&op!=="update"&&op!=="close"&&op!=="release")throw new PmCliError(`Mutation ${index+1} op must be create, update, close, or release.`,EXIT_CODE.USAGE);const options=validateMutationOptions(mutation.options,op,index,runtimeFields),unique=validateMutationKeyGuard(mutation.unique,op,index,"unique"),match=validateMutationKeyGuard(mutation.match,op,index,"match");if(typeof id!="string"||id.trim().length===0)throw new PmCliError(`Mutation ${index+1} requires a non-empty id.`,EXIT_CODE.USAGE);const normalizedId=id.trim();if(op==="close"){if(typeof reason!="string"||reason.trim().length===0)throw new PmCliError(`Mutation ${index+1} close requires a non-empty reason.`,EXIT_CODE.USAGE);return{op,id:normalizedId,reason:reason.trim(),...options===void 0?{}:{options}}}if(op==="release")return{op,id:normalizedId,...options===void 0?{}:{options}};if(!isPlainObject(options))throw new PmCliError(`Mutation ${index+1} ${op} requires an options object.`,EXIT_CODE.USAGE);return{op,id:normalizedId,options,...unique===void 0?{}:{unique},...match===void 0?{}:{match}}}function parseItemMutationBatch(input,parseOptions={}){const parsed=parseJsonValue(input,"Mutation batch"),rows=Array.isArray(parsed)?parsed:isPlainObject(parsed)&&Array.isArray(parsed.mutations)?parsed.mutations:void 0;if(rows===void 0||rows.length===0)throw new PmCliError("Mutation batch must be a non-empty JSON array or an object with a non-empty mutations array.",EXIT_CODE.USAGE);const runtimeFields=parseOptions.runtimeFields??[];return rows.map((row,index)=>{const mutation=validateMutationRow(row,index,runtimeFields);return mutation.options!==void 0&&preserveMutationStdinTokenLiterals(mutation.options),mutation})}function deriveReferencedItemId(transactionId,reference,idPrefix){const digest=crypto.createHash("sha256").update(transactionId).update("\0").update(reference).digest("hex"),token=BigInt(`0x${digest.slice(0,16)}`).toString(36).padStart(13,"0").slice(-12);return`${normalizePrefix(idPrefix)}${token}`}function replaceExactMutationReference(value,references){if(typeof value!="string"||!value.startsWith("@"))return value;const reference=value.slice(1),resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "${value}".`,EXIT_CODE.USAGE);return resolved}function replaceDependencyReference(value,references){if(typeof value!="string")return value;const resolvedValue=value.replace(MUTATION_DEPENDENCY_REFERENCE_PATTERN,(_match,prefix,reference)=>{const resolved=references[reference];if(resolved===void 0)throw new PmCliError(`Unknown mutation reference "@${reference}".`,EXIT_CODE.USAGE);return`${prefix}id=${resolved}`});if(/(^|,)id=@/u.test(resolvedValue))throw new PmCliError(`Malformed mutation reference in dependency "${value}".`,EXIT_CODE.USAGE);return resolvedValue}function referencedAliases(value){return typeof value!="string"?[]:value.startsWith("@")?[value.slice(1)]:[...value.matchAll(MUTATION_DEPENDENCY_REFERENCE_PATTERN)].map(match=>match[1]).filter(reference=>reference!==void 0)}function assertAcyclicCreateReferences(rows,inputReferences){const graph=new Map;for(const[index,row]of rows.entries()){const reference=inputReferences[index];if(reference===void 0)continue;const mutationOptions=isPlainObject(row.options)?row.options:{},dependencies=Array.isArray(mutationOptions.dep)?mutationOptions.dep:mutationOptions.dep===void 0?[]:[mutationOptions.dep];graph.set(reference,[mutationOptions.parent,mutationOptions.blockedBy,...dependencies].flatMap(referencedAliases))}const visiting=new Set,visited=new Set,visit=reference=>{if(visiting.has(reference))throw new PmCliError(`Mutation reference cycle includes "@${reference}".`,EXIT_CODE.USAGE);if(!visited.has(reference)){visiting.add(reference);for(const dependency of graph.get(reference))graph.has(dependency)&&visit(dependency);visiting.delete(reference),visited.add(reference)}};for(const reference of graph.keys())visit(reference)}function parseJsonLinesRows(input){const candidates=input.split(/\r?\n/u).map((line,index)=>({line:line.trim(),number:index+1})).filter(entry=>entry.line.length>0);if(candidates.length<2)return null;const parsed=candidates.map(entry=>{try{return{...entry,ok:!0,value:JSON.parse(entry.line)}}catch(error){return{...entry,ok:!1,error}}}),first=parsed[0];if(!first.ok&&(first.line.startsWith("[")||first.line==="{"||parsed.at(-1).line==="}")||!parsed.some(entry=>entry.ok&&isPlainObject(entry.value)))return null;const rows=[];for(const entry of parsed){if(!entry.ok)throw new PmCliError(`Mutation document line ${entry.number} must be one valid JSON object: ${describeUnknownError(entry.error)}`,EXIT_CODE.USAGE,{code:"stdin_json_invalid",required:"Provide one JSON object per line (JSON Lines), a JSON array, or a {schema_version, mutations} document.",why:"Structured mutation input must parse deterministically before any tracker write begins."});if(!isPlainObject(entry.value))throw new PmCliError(`Mutation document line ${entry.number} must be a JSON object.`,EXIT_CODE.USAGE);rows.push(entry.value)}return rows}function isMutationEnvelope(value){return Object.hasOwn(value,"mutations")||Object.hasOwn(value,"schema_version")}function parseMutationDocumentEnvelope(input){let parsed;try{parsed=parseJsonValue(input,"Mutation document")}catch(error){const rows=parseJsonLinesRows(input);if(rows===null)throw error;return{schema_version:1,mutations:rows}}return Array.isArray(parsed)?{schema_version:1,mutations:parsed}:isPlainObject(parsed)&&!isMutationEnvelope(parsed)?{schema_version:1,mutations:[parsed]}:parsed}function normalizeDocumentRow(row,index){const normalized=Object.hasOwn(row,"op")?validateOperationRow(row,index):normalizeCreateDocumentRow(row,index);if(normalized.op==="create"&&normalized.id===void 0&&isPlainObject(normalized.item)&&normalized.item.id!==void 0){if(typeof normalized.item.id!="string")throw new PmCliError(`Mutation ${index+1} item id must be a string.`,EXIT_CODE.USAGE);normalized.id=normalized.item.id}return normalized}function validateOperationRow(row,index){for(const key of Object.keys(row))if(!DOCUMENT_MUTATION_ROW_KEYS.includes(key))throw unknownKeyError(`mutation ${index+1}`,key,DOCUMENT_MUTATION_ROW_KEYS);if(row.item!==void 0&&row.op!=="create")throw new PmCliError(`Mutation ${index+1} item documents are only valid for create operations.`,EXIT_CODE.USAGE);if(row.item!==void 0&&row.options!==void 0)throw new PmCliError(`Mutation ${index+1} must declare either item or options, not both.`,EXIT_CODE.USAGE);return{...row}}function normalizeCreateDocumentRow(row,index){const{ref,id,item,...fields}=row,document=item!==void 0?item:fields;if(item!==void 0&&Object.keys(fields).length>0)throw new PmCliError(`Mutation ${index+1} must not mix item document keys with top-level fields.`,EXIT_CODE.USAGE);if(!isPlainObject(document))throw new PmCliError(`Mutation ${index+1} item document must be an object.`,EXIT_CODE.USAGE);return{op:"create",...ref===void 0?{}:{ref},...id===void 0?{}:{id},item:document}}function parseMutationDocumentRows(input){const envelope=parseMutationDocumentEnvelope(input);if(!isPlainObject(envelope))throw new PmCliError("Mutation document must be a JSON array or object.",EXIT_CODE.USAGE);for(const key of Object.keys(envelope))if(!MUTATION_DOCUMENT_KEYS.includes(key))throw unknownKeyError("mutation document",key,MUTATION_DOCUMENT_KEYS);if(envelope.schema_version!==void 0&&envelope.schema_version!==1)throw new PmCliError("Mutation document schema_version must be 1.",EXIT_CODE.USAGE);if(!Array.isArray(envelope.mutations)||envelope.mutations.length===0)throw new PmCliError("Mutation document requires a non-empty mutations array.",EXIT_CODE.USAGE);return envelope.mutations.map((row,index)=>{if(!isPlainObject(row))throw new PmCliError(`Mutation ${index+1} must be an object.`,EXIT_CODE.USAGE);return normalizeDocumentRow(row,index)})}function excludeOwnCreateIds(upsert,rows,options){const ownIds=new Set(Object.values(collectMutationReferences(rows,options,Object.create(null))));for(const row of rows)row.op==="create"&&typeof row.id=="string"&&row.id.trim().length>0&&ownIds.add(normalizeItemId(row.id,options.idPrefix));return{...upsert,existing:upsert.existing.filter(candidate=>!ownIds.has(candidate.id))}}function synthesizeMissingReferences(rows){const declared=new Set(rows.map(row=>row.ref).filter(ref=>typeof ref=="string"));for(const[index,row]of rows.entries()){if(row.op!=="create"||row.ref!==void 0||typeof row.id=="string"&&row.id.trim().length>0)continue;const synthesized=`${SYNTHESIZED_REFERENCE_PREFIX}${index+1}`;if(declared.has(synthesized))throw new PmCliError(`Mutation ${index+1} receives the synthesized ref "${synthesized}", but another row declares that ref. Rename it or give mutation ${index+1} its own ref; ${SYNTHESIZED_REFERENCE_PREFIX}<n> aliases are reserved for rows without one.`,EXIT_CODE.USAGE);row.ref=synthesized}}function itemDocumentToBatchOptions(item,mode,runtimeFields){const converted=itemDocumentToMutationOptions(JSON.stringify(item),mode,{},runtimeFields);return delete converted.stdinJson,delete converted.documentAnnotationSnapshot,converted}function runtimeFieldOptionKeys(op,runtimeFields){return op!=="create"&&op!=="update"?[]:runtimeFields.filter(definition=>definition.commands.includes(op)).map(definition=>runtimeFieldOptionTarget(definition))}function convertItemDocumentRows(rows,runtimeFields){for(const row of rows)isPlainObject(row.item)&&(row.options=itemDocumentToBatchOptions(row.item,"create",runtimeFields),delete row.item)}function itemDocumentMetadataKey(key,runtimeFields){return runtimeFields.some(field=>field.metadata_key===key)?key:resolveCoreUpsertMetadataKey(key)}function readUpsertKeyValue(row,key,runtimeFields){const camel=key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase());if(isPlainObject(row.item)){if(row.item[key]!==void 0)return row.item[key];if(itemDocumentMetadataKey(camel,runtimeFields)===key&&row.item[camel]!==void 0)return row.item[camel]}const options=isPlainObject(row.options)?row.options:{},declared=runtimeFields.find(field=>field.metadata_key===key);return declared!==void 0&&options[runtimeFieldOptionTarget(declared)]!==void 0?options[runtimeFieldOptionTarget(declared)]:options[key]!==void 0?options[key]:options[camel]!==void 0?options[camel]:(Array.isArray(options.field)?options.field:[]).map(String).find(entry=>entry.startsWith(`${key}=`))?.slice(key.length+1)}function compareCodeUnits(left,right){return left<right?-1:+(left!==right)}function canonicalizeUpsertValue(value){return Array.isArray(value)?value.map(canonicalizeUpsertValue):isPlainObject(value)?Object.fromEntries(Object.entries(value).sort(([left],[right])=>compareCodeUnits(left,right)).map(([key,nested])=>[key,canonicalizeUpsertValue(nested)])):value}function stableUpsertValue(value){return typeof value=="string"?value.trim():JSON.stringify(canonicalizeUpsertValue(value))}function applyKeyedUpsert(rows,upsert,idPrefix,runtimeFields,references){const key=upsert.key.trim();if(key.length===0)throw new PmCliError("Upsert key must be a non-empty field name.",EXIT_CODE.USAGE);const existingByValue=indexUpsertCandidates(upsert.existing),seen=new Map,upserted=new Set;for(const[index,row]of rows.entries()){if(row.op!=="create")continue;const stable=readStableUpsertKey(row,key,index,seen,runtimeFields),match=resolveUpsertMatch(existingByValue.get(stable)??[],key,stable);if(match===void 0){row.unique={key,value:readUpsertKeyValue(row,key,runtimeFields)};continue}const id=normalizeItemId(match,idPrefix);typeof row.ref=="string"&&(references[row.ref]=id),convertRowToUpsertUpdate(row,id,runtimeFields,{key,value:readUpsertKeyValue(row,key,runtimeFields)}),upserted.add(index)}return upserted}function indexUpsertCandidates(existing){const existingByValue=new Map;for(const candidate of existing){if(candidate.value===void 0||candidate.value===null)continue;const stable=stableUpsertValue(candidate.value);existingByValue.set(stable,[...existingByValue.get(stable)??[],candidate.id])}return existingByValue}function readStableUpsertKey(row,key,index,seen,runtimeFields){const value=readUpsertKeyValue(row,key,runtimeFields);if(value==null||value==="")throw new PmCliError(`Mutation ${index+1} is missing the upsert key "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_missing",required:`Every create record must carry a non-empty "${key}" value when --upsert-key ${key} is set.`,why:"Keyed upsert identifies existing items by that field; a record without it cannot be matched or safely created."});const stable=stableUpsertValue(value),previous=seen.get(stable);if(previous!==void 0)throw new PmCliError(`Mutations ${previous+1} and ${index+1} share the upsert key value ${JSON.stringify(stable)} for "${key}".`,EXIT_CODE.USAGE,{code:"upsert_key_duplicate",required:"Each record must carry a distinct upsert key value within one batch.",why:"Two records with the same key would race for the same existing item."});return seen.set(stable,index),stable}function resolveUpsertMatch(matches,key,stable){if(matches.length>1)throw new PmCliError(`Upsert key "${key}" value ${JSON.stringify(stable)} matches ${matches.length} existing items: ${matches.join(", ")}.`,EXIT_CODE.CONFLICT,{code:"upsert_key_ambiguous",required:"Resolve the duplicate existing items or choose a key that is unique per item.",why:"An ambiguous key cannot select one item to update without guessing."});return matches[0]}function convertRowToUpsertUpdate(row,id,runtimeFields,match){const updateKeys=new Set([...MUTATION_OPTION_KEYS.update,...runtimeFieldOptionKeys("update",runtimeFields)]),options=isPlainObject(row.item)?itemDocumentToBatchOptions(row.item,"update",runtimeFields):Object.fromEntries(Object.entries(row.options).filter(([optionKey])=>updateKeys.has(optionKey)));delete row.item,delete row.ref,row.op="update",row.id=id,row.options=options,row.match=match}function collectMutationReferences(rows,options,references){for(const[index,row]of rows.entries())if(row.ref!==void 0){if(row.op!=="create")throw new PmCliError(`Mutation ${index+1} ref is only valid for create operations.`,EXIT_CODE.USAGE);if(typeof row.ref!="string"||!MUTATION_REFERENCE_PATTERN.test(row.ref))throw new PmCliError(`Mutation ${index+1} ref must match ${MUTATION_REFERENCE_PATTERN.source}.`,EXIT_CODE.USAGE);if(references[row.ref]!==void 0)throw new PmCliError(`Duplicate mutation ref "${row.ref}".`,EXIT_CODE.USAGE);references[row.ref]=typeof row.id=="string"&&row.id.trim().length>0?normalizeItemId(row.id,options.idPrefix):deriveReferencedItemId(options.transactionId,row.ref,options.idPrefix)}return references}function resolveItemMutationDocument(input,options){const rows=parseMutationDocumentRows(input);synthesizeMissingReferences(rows);const inputReferences=rows.map(row=>typeof row.ref=="string"?row.ref:void 0),references=Object.create(null),upserted=options.upsert===void 0?new Set:applyKeyedUpsert(rows,excludeOwnCreateIds(options.upsert,rows,options),options.idPrefix,options.runtimeFields??[],references);convertItemDocumentRows(rows,options.runtimeFields??[]),collectMutationReferences(rows,options,references),assertAcyclicCreateReferences(rows,inputReferences);const resolvedRows=rows.map(row=>{const resolved={...row},reference=typeof resolved.ref=="string"?resolved.ref:void 0;if(delete resolved.ref,reference!==void 0?resolved.id=references[reference]:resolved.id=replaceExactMutationReference(resolved.id,references),isPlainObject(resolved.options)){const mutationOptions={...resolved.options};for(const key of["parent","blockedBy"])mutationOptions[key]!==void 0&&(mutationOptions[key]=replaceExactMutationReference(mutationOptions[key],references));Array.isArray(mutationOptions.dep)?mutationOptions.dep=mutationOptions.dep.map(entry=>replaceDependencyReference(entry,references)):mutationOptions.dep!==void 0&&(mutationOptions.dep=replaceDependencyReference(mutationOptions.dep,references)),resolved.options=mutationOptions}return resolved}),mutations=parseItemMutationBatch(JSON.stringify(resolvedRows),{runtimeFields:options.runtimeFields??[]}),createIds=mutations.filter(mutation=>mutation.op==="create").map(mutation=>mutation.id);if(new Set(createIds).size!==createIds.length)throw new PmCliError("Duplicate resolved create id in mutation document.",EXIT_CODE.USAGE);const receiptRows=mutations.map((mutation,index)=>{const reference=inputReferences[index];return{index,...typeof reference=="string"?{ref:reference}:{},id:mutation.id,op:mutation.op,...upserted.has(index)?{upserted:!0}:{}}});return{schema_version:1,mutations,references,rows:receiptRows}}function serializePairs(value,keys){return keys.filter(key=>value[key]!==void 0&&value[key]!==null).map(key=>{const rendered=String(value[key]);return`${key}=${/[",\n\r\\]/u.test(rendered)?JSON.stringify(rendered):rendered}`}).join(",")}function serializeObjectArray(value,keys,label){if(value!==void 0){if(!Array.isArray(value)||!value.every(isPlainObject))throw new PmCliError(`${label} must be an array of objects.`,EXIT_CODE.USAGE);return value.map(entry=>serializePairs(entry,keys))}}function scalarItemOption(key,value){return key==="tags"&&Array.isArray(value)&&value.every(entry=>typeof entry=="string")?JSON.stringify(value):typeof value=="number"||typeof value=="boolean"?String(value):value}function appendFacetOptions(target,source,skipPersistedAnnotations=!1){for(const[sourceKey,targetKey,keys]of ITEM_MUTATION_FACET_OPTION_MAPPINGS){const sourceValue=source[sourceKey],serialized=serializeObjectArray(skipPersistedAnnotations&&["comments","notes","learnings"].includes(sourceKey)&&Array.isArray(sourceValue)?sourceValue.filter(entry=>!isPlainObject(entry)||entry.created_at===void 0):sourceValue,keys,sourceKey);serialized!==void 0&&serialized.length>0&&(target[targetKey]=serialized)}}function validateItemDocumentKeys(parsed,envelope){for(const key of Object.keys(parsed)){if(envelope){if(!ITEM_ENVELOPE_KEYS.includes(key))throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS);continue}if(!ITEM_FIELD_KEYS.has(key)&&nearestKey(key,[...ITEM_FIELD_KEYS])===void 0&&nearestKey(key,ITEM_ENVELOPE_KEYS)==="item")throw unknownKeyError("item envelope",key,ITEM_ENVELOPE_KEYS)}}function resolveItemDocument(parsed){const envelope=Object.hasOwn(parsed,"item");validateItemDocumentKeys(parsed,envelope);const item=envelope?parsed.item:parsed;if(!isPlainObject(item))throw new PmCliError("Item document item must be an object.",EXIT_CODE.USAGE);return{item,...envelope&&isPlainObject(parsed.linked)?{linked:parsed.linked}:{}}}function appendItemDocumentEntry(documentOptions,key,value,mode,runtimeFields,explicitOptions){if((mode==="update"?UPDATE_READ_ONLY_ITEM_KEYS:READ_ONLY_ITEM_KEYS).has(key))return;const definition=runtimeFields.find(field=>field.metadata_key===key&&field.commands.includes(mode));if(definition!==void 0){const explicitValue=readRuntimeFieldOptionValue(explicitOptions,definition);documentOptions[runtimeFieldOptionTarget(definition)]=explicitValue!==void 0?explicitValue:definition.type==="array"||definition.type==="object"?JSON.stringify(value):value;return}if(!ITEM_FIELD_KEYS.has(key)){if(nearestKey(key,[...ITEM_FIELD_KEYS])!==void 0)throw unknownKeyError("item document",key,[...ITEM_FIELD_KEYS]);const fields=Array.isArray(documentOptions.field)?documentOptions.field:[];fields.push(`${key}=${typeof value=="string"?value:JSON.stringify(value)}`),documentOptions.field=fields;return}["dependencies","comments","notes","learnings","files","tests","docs","reminders","events","type_options"].includes(key)||(documentOptions[key.replaceAll(/_([a-z])/gu,(_match,letter)=>letter.toUpperCase())]=scalarItemOption(key,value))}function itemDocumentToMutationOptions(input,mode,explicitOptions={},runtimeFields=[]){const parsed=parseJsonValue(input,"Item document");if(!isPlainObject(parsed))throw new PmCliError("Item document must be a JSON object.",EXIT_CODE.USAGE);const{item,linked}=resolveItemDocument(parsed),documentOptions={};for(const[key,value]of Object.entries(item)){const metadataKey=itemDocumentMetadataKey(key,runtimeFields);metadataKey!==key&&Object.hasOwn(item,metadataKey)||appendItemDocumentEntry(documentOptions,metadataKey,value,mode,runtimeFields,explicitOptions)}if(appendFacetOptions(documentOptions,item,mode==="update"),linked!==void 0&&appendFacetOptions(documentOptions,linked),mode==="update"){const annotations=Object.fromEntries(["comments","notes","learnings"].filter(key=>Object.hasOwn(item,key)).map(key=>[key,item[key].filter(entry=>isPlainObject(entry)&&entry.created_at!==void 0)]));Object.keys(annotations).length>0&&(documentOptions.documentAnnotationSnapshot=annotations)}return{...documentOptions,...Object.fromEntries(Object.entries(explicitOptions).filter(([,value])=>value!==void 0)),...Array.isArray(documentOptions.field)&&Array.isArray(explicitOptions.field)?{field:[...documentOptions.field,...explicitOptions.field]}:{},stdinJson:void 0}}function validateItemMutationRows(value){return parseItemMutationBatch(JSON.stringify(value))}export{deriveMutationTransactionId,itemDocumentToMutationOptions,parseAtomicMutationControls,parseItemMutationBatch,resolveItemMutationDocument,stableUpsertValue,validateItemMutationRows};
|
|
2
2
|
//# sourceMappingURL=structured-mutations.js.map
|
package/docs/COMMANDS.md
CHANGED
|
@@ -890,6 +890,20 @@ Linked files and docs keep reviews reproducible. `deps` is read-only and project
|
|
|
890
890
|
|
|
891
891
|
Structured key/value forms reject unrecognized keys with an `Allowed keys: …` error (matching `test --add`), so a typoed key (`lable=` instead of `label=`) fails fast instead of being silently dropped: `--add`/`--file`/`--doc` accept `path,scope,note`; `--add-glob` accepts `pattern,glob,path,scope,note`; `--remove` accepts `path`; `--migrate` accepts `from,to`; create/update `--dep` accepts `id,kind,type,author,created_at,source_kind`; `--reminder` accepts `at,date,text,title`; `--event` accepts `start,date,end,duration,title,description,location,timezone,all_day` and the `recur_*` recurrence keys. Set `source_kind=global` for a dependency owned by another workspace: pm preserves its id verbatim, materializes it as an external graph endpoint, and excludes it from dangling-local-reference findings. Explicit remote locators also retain their spelling; other dependency and parent references share local `id_prefix` normalization, including uppercase and `#` shorthand. Bare values (`--add src/cli/main.ts`) skip key validation.
|
|
892
892
|
|
|
893
|
+
Context `meta.usedTokens` measures the selected built-in JSON or TOON emission,
|
|
894
|
+
including its newline, alias counts, omission receipt and optional discovery or
|
|
895
|
+
token-accounting metadata. If the required rendered packet cannot fit in the requested
|
|
896
|
+
token budget, `pm item deps` returns usage exit code 2 with
|
|
897
|
+
`invalid_argument_value`. Structured `recovery.token_budget` reports the requested
|
|
898
|
+
budget and a verified feasible budget. Its `minimum_status` is `exact` only after
|
|
899
|
+
every smaller positive budget is checked; bounded searches can instead report
|
|
900
|
+
`feasible_only` or `unavailable` with no invented minimum. When a feasible budget
|
|
901
|
+
is known, `suggested_retry_args` preserves the tracker, query and output options
|
|
902
|
+
in an executable context retry with that `--token-budget`. A separate
|
|
903
|
+
`next_best_command` provides a compact tree-summary inspection.
|
|
904
|
+
(Repository tracker: [pm-odw3j8](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-odw3j8.toon);
|
|
905
|
+
tracker records are excluded from npm.)
|
|
906
|
+
|
|
893
907
|
## Graph Queries
|
|
894
908
|
|
|
895
909
|
```bash
|
package/docs/PR_REVIEW_LOOP.md
CHANGED
|
@@ -15,6 +15,7 @@ capture, avoiding repeated diff hunks that are irrelevant to marker matching.
|
|
|
15
15
|
```bash
|
|
16
16
|
node scripts/reviews/pr-review-loop.mjs inventory --pr 123 > /tmp/pr-123-review-inventory.json
|
|
17
17
|
node scripts/reviews/pr-review-loop.mjs watch --pr 123 --interval 30 > /tmp/pr-123-review-inventory.json
|
|
18
|
+
node scripts/reviews/pr-review-loop.mjs watch --pr 123 --phase reviews --interval 30 > /tmp/pr-123-full-review-inventory.json
|
|
18
19
|
node scripts/reviews/pr-review-loop.mjs react --node-id IC_kw... --reaction THUMBS_UP
|
|
19
20
|
node scripts/reviews/pr-review-loop.mjs acknowledge --pr 123 --node-id PRR_kw... --revision '<revision>' --reaction THUMBS_UP --body "CodeRabbit feedback implemented: https://github.com/owner/repo/pull/123#pullrequestreview-456. The suggested edge case is covered by test X."
|
|
20
21
|
node scripts/reviews/pr-review-loop.mjs reply-inline --pr 123 --comment-id 456 --body "Addressed in abc123."
|
|
@@ -43,17 +44,34 @@ markers then distinguish revised feedback and suppress duplicate retry replies
|
|
|
43
44
|
for that same revision. Inline markers are matched only within the selected
|
|
44
45
|
reply thread. Legacy calls without the flag retain their existing behavior.
|
|
45
46
|
|
|
46
|
-
After every push or reviewer retrigger, run `watch`.
|
|
47
|
-
`gh pr checks --watch`,
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
After every push or reviewer retrigger, run `watch`. Its default `checks` phase
|
|
48
|
+
delegates waiting to `gh pr checks --watch --fail-fast`, returning the complete
|
|
49
|
+
exact-head conversation inventory as soon as a check fails, even while an optional
|
|
50
|
+
reviewer remains pending. Fix that failure before starting another wait. Use
|
|
51
|
+
`--phase reviews` for the separate blocking review-completion phase: it delegates
|
|
52
|
+
to `gh pr checks --watch` without fail-fast and waits for every emitted check.
|
|
53
|
+
Both phases record `pendingChecks` and `unavailableChecks` from the final rollup,
|
|
54
|
+
including names, states, URLs and whether each context is required. Neutral,
|
|
55
|
+
skipped, canceled and other unavailable signals do not establish review approval;
|
|
56
|
+
inspect the retained conversations for provider quota or service limitations.
|
|
57
|
+
Pending checks prevent a successful receipt even if GitHub reports `CLEAN`.
|
|
58
|
+
This behavior is tracked by [pm-jm7m3o](../.agents/pm/tasks/pm-jm7m3o.toon).
|
|
59
|
+
Native watch success covers emitted checks only: a required provider may
|
|
50
60
|
never emit its context. The helper unions classic branch-protection requirements
|
|
51
|
-
with all effective ruleset requirements
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
with all effective ruleset requirements and retains publisher bindings in
|
|
62
|
+
`mergeReadiness.requiredChecks` alongside `requiredContexts`. Bound requirements
|
|
63
|
+
need both a name in the head rollup and paginated exact-head check-run evidence
|
|
64
|
+
from the expected app. A wrong or unavailable check-run publisher remains in
|
|
65
|
+
`missingContexts`; unbound legacy contexts retain name-based presence checks.
|
|
66
|
+
Commit statuses expose no app identity in the rollup. Status-only bound contexts
|
|
67
|
+
are disclosed in `publisherUnverifiable` with source `commit_status` and rely on
|
|
68
|
+
GitHub's `CLEAN` verdict to enforce the publisher binding. If the same context is
|
|
69
|
+
also emitted as a check run, that run still requires exact-head app evidence.
|
|
70
|
+
The receipt records GitHub's `mergeStateStatus` independently: an attempt is
|
|
71
|
+
`passed` only when no requirement is missing and GitHub reports `CLEAN`.
|
|
72
|
+
Publisher presence does not establish a passing verdict; GitHub retains that
|
|
73
|
+
enforcement along with other merge requirements. Publisher evidence is tracked by
|
|
74
|
+
[pm-5p48kf](../.agents/pm/tasks/pm-5p48kf.toon). Missing contexts and blocked or
|
|
57
75
|
unknown merge state remain `incomplete`; native watch failures remain `failed`.
|
|
58
76
|
Superseded attempts carry `superseded: true` and never certify readiness. Unavailable policy or status reads
|
|
59
77
|
fail visibly instead of certifying readiness. Successful
|
|
@@ -62,8 +80,10 @@ The direct `watch` command emits its complete JSON receipt before exiting `1`
|
|
|
62
80
|
for `incomplete` or `failed`, and exits `0` only for `passed`. Shell automation
|
|
63
81
|
can therefore stop on the exit status while retaining all findings for triage.
|
|
64
82
|
|
|
65
|
-
A failed
|
|
66
|
-
|
|
83
|
+
A failed check is still an actionable signal: `watch` records the failed outcome
|
|
84
|
+
and returns all findings instead of aborting before inventory. A fail-fast receipt
|
|
85
|
+
does not establish completion of pending reviews; the explicit review phase and
|
|
86
|
+
final inventory retain that separate obligation.
|
|
67
87
|
If the PR head or target branch changes during the wait or final readiness read,
|
|
68
88
|
the helper watches the new target, up to three consecutive attempts, before
|
|
69
89
|
returning exact-head state. It does not poll an absent required provider status.
|
package/docs/SCRIPTING.md
CHANGED
|
@@ -101,6 +101,16 @@ receipts without that evidence retain their existing envelope.
|
|
|
101
101
|
|
|
102
102
|
JSON object field order is not an API. Consume fields by name. Read envelopes keep the stable pagination vocabulary `items`, `count`, `total`, `has_more`, and, when another page exists, `next_cursor`. The `filters` object echoes the effective query scope. Plain `pm list` and `pm search` are all-status reads and disclose `filters.status: "all"`; lifecycle-specific commands such as `pm list-open` remain explicit shortcuts.
|
|
103
103
|
|
|
104
|
+
Default brief-list TOON presentation omits default projection, sorting, timestamp,
|
|
105
|
+
and redundant zero-count metadata. A `details: "--full"` pointer replaces only
|
|
106
|
+
the standard full-item omission receipt. Unchecked source status, pagination,
|
|
107
|
+
non-default sorting, warnings, and additional diagnostics remain visible.
|
|
108
|
+
Use JSON for the complete typed envelope. TOON object rows preserve explicit
|
|
109
|
+
nulls and empty values, including nested cells. Only genuinely missing keys
|
|
110
|
+
appear in a table's `absent` metadata.
|
|
111
|
+
Successful health checks retain their compact presentation by omitting an empty
|
|
112
|
+
diagnostic `details` object. Material diagnostics and failing checks remain visible.
|
|
113
|
+
|
|
104
114
|
Projection flags intentionally change row shape. Use `--fields` when a script requires an exact subset, `--brief` or `--compact` only when the documented sparse shape is sufficient, and `--full` when linked metadata is required. Check `row_contract` on generic read surfaces that expose one; do not infer omitted fields as empty values.
|
|
105
115
|
|
|
106
116
|
For long-lived clients, generate or inspect the runtime contract instead of hard-coding recalled flags:
|
package/docs/SDK.md
CHANGED
|
@@ -1382,6 +1382,22 @@ handlers remain disabled. Package-backed `--template` create input cannot be
|
|
|
1382
1382
|
semantically previewed through its package handler; expand it into explicit core
|
|
1383
1383
|
mutation options before preview.
|
|
1384
1384
|
|
|
1385
|
+
Actionable previews are tracked by [pm-ekf2gu](../.agents/pm/issues/pm-ekf2gu.toon)
|
|
1386
|
+
and [pm-fa9olc](../.agents/pm/tasks/pm-fa9olc.toon).
|
|
1387
|
+
`previewItemMutations` returns `warnings` grouped by zero-based operation
|
|
1388
|
+
`index`, canonical `id`, and `op`. These are the original create lifecycle
|
|
1389
|
+
warnings, including advisory `likely_duplicates` and duplicate-recovery tokens.
|
|
1390
|
+
Earlier staged creates participate in later duplicate checks. Strict policy
|
|
1391
|
+
still refuses duplicates; preview never changes the configured policy.
|
|
1392
|
+
|
|
1393
|
+
For a document resolved with `planItemMutationDocument`, call
|
|
1394
|
+
`previewItemMutationDocument({ pmRoot, author, plan })` from the aggregate SDK.
|
|
1395
|
+
It returns `{ validation, rows }`; affected rows retain their source `index`
|
|
1396
|
+
and `ref` and gain `warnings`. Rows without warnings omit that property. The
|
|
1397
|
+
input plan stays unchanged. Both `pm item mutate --dry-run` and MCP `pm_mutate`
|
|
1398
|
+
use this same projection. A `replayed_committed_plan` has no newly evaluated
|
|
1399
|
+
warnings; an empty warning list in that state does not certify current policy.
|
|
1400
|
+
|
|
1385
1401
|
Staging excludes derived `search` and `locks` content and unrelated runtime state,
|
|
1386
1402
|
retaining registered item folders, explicitly configured schema files, and
|
|
1387
1403
|
`runtime/session.json` for core focus and inherited-parent semantics. Root-layout
|
|
@@ -1395,6 +1411,17 @@ remapped only in the staged settings. Schema files configured outside the
|
|
|
1395
1411
|
tracker fail with `transaction_preview_external_schema`; relocate them inside
|
|
1396
1412
|
the selected tracker root before retrying.
|
|
1397
1413
|
|
|
1414
|
+
`transaction_preview_non_regular_file` includes `preview_storage` in SDK error
|
|
1415
|
+
context and the CLI JSON error envelope. It reports a fixed `storage_area`,
|
|
1416
|
+
`entry_redacted`, and, when safe, `relative_entry` using `/` separators relative
|
|
1417
|
+
to the selected tracker root. Coordinates are limited to 160 ASCII characters
|
|
1418
|
+
from letters, digits, `.`, `_`, `-`, and separators. Absolute paths, traversal,
|
|
1419
|
+
control characters and overlong or otherwise unsafe names are withheld; the
|
|
1420
|
+
storage category remains available. Diagnostics never include link targets.
|
|
1421
|
+
Inspect that entry without following links, restore the intended regular file
|
|
1422
|
+
or directory, and retry the dry run. Native filesystem errors retain their
|
|
1423
|
+
existing diagnostics.
|
|
1424
|
+
|
|
1398
1425
|
Preview requires trusted, stable directory entries and ancestors. Where Node
|
|
1399
1426
|
exposes `O_NOFOLLOW` and `O_NONBLOCK`, descriptor opens request those POSIX guards.
|
|
1400
1427
|
Windows retains entry checks, regular-file descriptor checks and snapshot
|
|
@@ -1640,6 +1667,13 @@ trees, custom runtime fields, dependency-aware filtering, unsupported filters,
|
|
|
1640
1667
|
and stale/missing indexes take the canonical fallback path with identical
|
|
1641
1668
|
result contracts. Custom hosts may call `queryItemMetadataIndex` directly, but
|
|
1642
1669
|
must treat `null` as an instruction to rebuild or use authoritative storage.
|
|
1670
|
+
The separate history-event index is also optional. Recognized native SQLite
|
|
1671
|
+
failures while starting its write transaction invalidate that projection before
|
|
1672
|
+
appending authoritative history. Native lock contention retains its existing
|
|
1673
|
+
invalidation protocol; unrelated adapter exceptions and authoritative append
|
|
1674
|
+
failures propagate. A later index rebuild restores accelerated history reads.
|
|
1675
|
+
(Repository tracker: [pm-0orupq](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-0orupq.toon);
|
|
1676
|
+
tracker records are excluded from npm.)
|
|
1643
1677
|
|
|
1644
1678
|
List results always expose `total`, `has_more`, `truncated`, and a nullable
|
|
1645
1679
|
`next_cursor`, whether or not the page was truncated; unset filter echoes are
|
|
@@ -1688,6 +1722,16 @@ true. The normal JSON envelope remains unchanged, and an actionable cursor is
|
|
|
1688
1722
|
always retained. SDK query functions continue returning their full typed
|
|
1689
1723
|
results; lean projection belongs to the CLI presentation boundary.
|
|
1690
1724
|
|
|
1725
|
+
Custom agent hosts can import `projectPmBriefListEnvelope` from
|
|
1726
|
+
`@unbrained/pm-cli/sdk` to apply the same brief-list presentation without
|
|
1727
|
+
mutating the original result. It retains unchecked source status and actionable
|
|
1728
|
+
continuation, and leaves partial, custom, or inconsistent envelopes intact.
|
|
1729
|
+
The CLI applies this projection only to TOON; ordinary JSON and SDK query
|
|
1730
|
+
results retain their complete contracts. The public `encodePmTableRows` and
|
|
1731
|
+
`decodePmTableRows` primitives distinguish explicit null and empty cells from
|
|
1732
|
+
missing keys. TOON rendering preserves those row facts before selecting the
|
|
1733
|
+
smaller expanded or tabular encoding.
|
|
1734
|
+
|
|
1691
1735
|
Migration note: before this contract, `total` was present only when pagination
|
|
1692
1736
|
omitted rows, a completed page omitted `next_cursor`, and verbose results
|
|
1693
1737
|
emitted unset filter keys with `null` values. Consumers must use `has_more` or
|
|
@@ -2468,6 +2512,39 @@ throw createPmCliExpectedError("hello requires --name", {
|
|
|
2468
2512
|
|
|
2469
2513
|
The helper returns an `Error` whose public name is `PmCliError` and whose `exitCode` is structural. That makes it safe for bundled, linked, and separately installed package code even when class identity is not shared with the running CLI.
|
|
2470
2514
|
|
|
2515
|
+
For example, the SDK command layer settles relationship token counts after alias
|
|
2516
|
+
counts, read disclosure, encoding and optional lean/token-accounting policies.
|
|
2517
|
+
It converts an impossible rendered context budget into an expected
|
|
2518
|
+
`invalid_argument_value` usage error. The public `PmTokenBudgetRecovery` type and
|
|
2519
|
+
`isPmTokenBudgetRecovery` guard describe `context.recovery.token_budget`: requested
|
|
2520
|
+
tokens, a verified feasible retry value, and an exact minimum only when every
|
|
2521
|
+
smaller positive budget has been checked. Larger searches report `feasible_only`;
|
|
2522
|
+
query cost limits, exhausted searches or independent output constraints can report
|
|
2523
|
+
`unavailable`. Both states leave the unproved minimum `null`. The bounded search
|
|
2524
|
+
uses the real packer and output policy, including alias disclosure.
|
|
2525
|
+
The public type is a discriminated union: checking `minimum_status` narrows the
|
|
2526
|
+
verified budgets and requires a reason for `unavailable`. Package authors cannot
|
|
2527
|
+
declare unavailable advice with a numeric budget or omit its cause. The runtime
|
|
2528
|
+
guard additionally checks positive safe integers and equal budgets for `exact`;
|
|
2529
|
+
those numeric invariants cannot be expressed by the declaration alone.
|
|
2530
|
+
`suggested_retry_args` preserves the selected tracker, query and output options
|
|
2531
|
+
with the verified feasible budget so it works from another workspace.
|
|
2532
|
+
Retry probes consume canonical summary/full include modes before packing and
|
|
2533
|
+
retain only residual field selectors, matching the emitted CLI command. Recovery
|
|
2534
|
+
work estimates include repeated dangling-reference renders; summary probes count
|
|
2535
|
+
only their single rendering pass. Expensive queries return explicit unavailable
|
|
2536
|
+
budget advice and retain the compact inspection fallback.
|
|
2537
|
+
`next_best_command` retains a separate compact tree-summary inspection, which
|
|
2538
|
+
also supplies the fallback when no working context budget is established.
|
|
2539
|
+
CLI JSON and MCP preserve that recovery. The lower-level
|
|
2540
|
+
`buildRelationshipContext` primitive retains its `TypeError` validation contract;
|
|
2541
|
+
unrelated runtime errors still propagate.
|
|
2542
|
+
(Repository tracker: [pm-odw3j8](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-odw3j8.toon),
|
|
2543
|
+
[pm-0bbc53](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-0bbc53.toon),
|
|
2544
|
+
[pm-k6uglj](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-k6uglj.toon),
|
|
2545
|
+
[pm-8jzcwv](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-8jzcwv.toon);
|
|
2546
|
+
tracker records are excluded from npm.)
|
|
2547
|
+
|
|
2471
2548
|
For policy, lint, or verification commands that should render their full result
|
|
2472
2549
|
and report failure without throwing, return a structured object instead:
|
|
2473
2550
|
|
package/docs/TESTING.md
CHANGED
|
@@ -49,6 +49,13 @@ claims to the same canonical gate IDs. Hosted-only environment isolation and
|
|
|
49
49
|
tracker-integrity steps remain explicit entries with reasons rather than
|
|
50
50
|
silently disappearing from local parity.
|
|
51
51
|
|
|
52
|
+
The full-source command-example scan runs after parallel contract files in the
|
|
53
|
+
existing serial cohort. It keeps the same source corpus, assertions and
|
|
54
|
+
30-second deadline, so concurrent contract workers do not consume its
|
|
55
|
+
policy-check budget. This allocation is tracked by
|
|
56
|
+
[pm-a3c6g4](../.agents/pm/tasks/pm-a3c6g4.toon); broader runner qualification
|
|
57
|
+
remains with [pm-bj7rq0](../.agents/pm/issues/pm-bj7rq0.toon).
|
|
58
|
+
|
|
52
59
|
`node scripts/run-tests.mjs` wraps Vitest in temporary tracker roots, disables
|
|
53
60
|
external Sentry delivery for the build, test workers, and their nested CLI
|
|
54
61
|
children, then cleans the roots up. Instrumentation tests can still exercise
|
|
@@ -244,6 +251,22 @@ into the pass count; a persistent assertion still fails after the bounded
|
|
|
244
251
|
retry. Local runs do not retry, so deterministic failures stay immediate while
|
|
245
252
|
iterating.
|
|
246
253
|
|
|
254
|
+
The complete receipt-operation, aborted-operation recovery, receipt-settlement, merge-portability and
|
|
255
|
+
lossless-collection-union integration files run in the existing serial
|
|
256
|
+
`lifecycle-performance` project after parallel contracts. Both nightly shards and
|
|
257
|
+
full coverage use that same project partition, retaining every case and its
|
|
258
|
+
original assertions, deadlines and retry policy. This allocation is tracked by
|
|
259
|
+
[pm-89297t](../.agents/pm/tasks/pm-89297t.toon); it does not classify or erase
|
|
260
|
+
historical failed attempts.
|
|
261
|
+
|
|
262
|
+
The complete macOS Node 24 and Windows Node 24 nightly suites each use two
|
|
263
|
+
Vitest shards. Both halves must pass within the existing 20-minute job limit;
|
|
264
|
+
sharding retains the same complete file population, serial phase, assertions,
|
|
265
|
+
per-test deadlines and retry policy. The recurrence collector retains the old
|
|
266
|
+
unsharded platform families alongside both current shard identities, so changing
|
|
267
|
+
the matrix does not erase earlier failures. macOS allocation is tracked by
|
|
268
|
+
[pm-qrirz8](../.agents/pm/tasks/pm-qrirz8.toon).
|
|
269
|
+
|
|
247
270
|
Vitest's GitHub reporter emits annotations and retry evidence. The repository
|
|
248
271
|
reliability reporter additionally writes
|
|
249
272
|
`.vitest-reports/reliability-<shard>.json` and appends a job-summary table with
|
|
@@ -668,7 +691,8 @@ pm test <item-id> --run --workspace-context snapshot --override-linked-workspace
|
|
|
668
691
|
Windows execution is tracked by [pm-bj7rq0](../.agents/pm/issues/pm-bj7rq0.toon).
|
|
669
692
|
Script and native keeper cohorts run independently on Windows Node 24. Their
|
|
670
693
|
source/script keeper steps retain the two-worker cap, and each cohort retains
|
|
671
|
-
the 12-minute deadline. The
|
|
694
|
+
the 12-minute deadline. The four-file Git/worktree receipt slice uses one
|
|
695
|
+
worker to avoid competing subprocess-heavy files at the same case deadlines. The required
|
|
672
696
|
`Windows regression (Node 24)` check waits for both cohorts and fails if either
|
|
673
697
|
fails, is cancelled, or is skipped. This preserves every selected regression
|
|
674
698
|
while avoiding a shared serial job deadline. See GitHub's
|
package/marketplace.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"pm","owner":{"name":"unbrained","url":"https://github.com/unbraind/pm-cli"},"description":"Official marketplace for pm CLI — native git-based project management for Claude Code and AI coding agents.","version":"2026.10.
|
|
1
|
+
{"name":"pm","owner":{"name":"unbrained","url":"https://github.com/unbraind/pm-cli"},"description":"Official marketplace for pm CLI — native git-based project management for Claude Code and AI coding agents.","version":"2026.10.20","plugins":[{"name":"pm-claude","source":"./plugins/pm-claude","description":"Native pm CLI integration for Claude Code — 32 MCP tools, 14 workflow skills with slash invocations, 4 subagents, hybrid TUI task tracking, session context injection, and coordination subagents for git-based project management without leaving Claude Code.","version":"2026.10.20","author":{"name":"unbrained","url":"https://github.com/unbraind/pm-cli"},"homepage":"https://github.com/unbraind/pm-cli","repository":"https://github.com/unbraind/pm-cli","license":"MIT","keywords":["pm-cli","project-management","mcp","tasks","agents","workflows","git-native","task-tracker","ai"],"category":"productivity"}]}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-beads","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-beads","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package for importing Beads JSONL records.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-beads#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-beads"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","beads"],"pm":{"aliases":["beads"],"extensions":["extensions/beads"],"catalog":{"display_name":"Beads Import","category":"migration","summary":"Import Beads JSONL records into pm while preserving source IDs.","tags":["migration","beads","import"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-beads#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-beads","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-calendar","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-calendar","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package for calendar and agenda views.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-calendar#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-calendar"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","calendar","agenda"],"pm":{"aliases":["calendar"],"extensions":["extensions/calendar"],"catalog":{"display_name":"Calendar Views","category":"workflow","summary":"Show agenda, day, week, and month views for deadlines, reminders, and scheduled events.","tags":["calendar","agenda","workflow"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-calendar#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-calendar","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-command-kit","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-command-kit","version":"2026.10.20","private":true,"type":"module","description":"First-party pm exemplar package for the commands capability: registerCommand, registerFlags, and registerParser.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-command-kit#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-command-kit"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","sdk","commands","exemplar"],"pm":{"aliases":["command-kit"],"extensions":["extensions/command-kit"],"catalog":{"display_name":"Command Kit Exemplar","category":"sdk","summary":"Copy-paste-able exemplar for registerCommand, registerFlags, and registerParser.","tags":["sdk","commands","parser","exemplar"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-command-kit#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-command-kit","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-digital-twin","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-digital-twin","version":"2026.10.20","private":true,"type":"module","description":"Beyond-PM exemplar: a temporal production-facility digital twin built entirely on public pm SDK and extension primitives.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-digital-twin#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-digital-twin"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","sdk","digital-twin","event-sourcing","temporal-graph","exemplar"],"pm":{"aliases":["digital-twin","twin"],"extensions":["extensions/digital-twin"],"catalog":{"display_name":"Digital Twin SDK Exemplar","category":"sdk","summary":"Temporal facility graph proving event replay, point-in-time state, invariants, federation, checkpoints, and compact exports through public SDK contracts.","tags":["sdk","digital-twin","event-sourcing","graph","offline-first","exemplar"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-digital-twin#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-digital-twin","report":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-digital-twin/GAP_REPORT.md"}},"docs":["README.md","GAP_REPORT.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-governance-audit","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-governance-audit","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package for governance audit and normalization workflows.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-governance-audit#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-governance-audit"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","governance","audit","normalize","hooks"],"pm":{"aliases":["governance-audit","audit"],"extensions":["extensions/governance-audit"],"catalog":{"display_name":"Governance Audit","category":"governance","summary":"Run dedupe audits, comments audits, and normalize sweeps as package-owned workflows.","tags":["governance","audit","normalize","hooks"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-governance-audit#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-governance-audit","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-guide-shell","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-guide-shell","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package for guide and shell completion workflows.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-guide-shell#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-guide-shell"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","guide","completion","shell"],"pm":{"aliases":["guide-shell"],"extensions":["extensions/guide-shell"],"catalog":{"display_name":"Guide + Shell UX","category":"workflow","summary":"Restore guide documentation and shell completion commands as package-owned UX surfaces.","tags":["guide","completion","shell"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-guide-shell#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-guide-shell","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-kanban","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-kanban","version":"2026.10.20","private":true,"type":"module","description":"First-party pm archetype package: a complete Kanban continuous-flow profile built on public SDK primitives.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-kanban#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-kanban"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","sdk","profile","archetype","kanban","exemplar"],"pm":{"aliases":["kanban"],"extensions":["extensions/kanban"],"catalog":{"display_name":"Kanban Archetype","category":"sdk","summary":"Installable Kanban continuous-flow archetype: Card item type, flow fields, and a ProjectProfileDefinition built on public SDK primitives.","tags":["sdk","profile","archetype","kanban","schema","exemplar"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-kanban#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-kanban","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-lifecycle-hooks","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-lifecycle-hooks","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package demonstrating safe lifecycle hook registration.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-lifecycle-hooks#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-lifecycle-hooks"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","hooks","lifecycle","sdk"],"pm":{"aliases":["lifecycle-hooks"],"extensions":["extensions/lifecycle-hooks"],"catalog":{"display_name":"Lifecycle Hooks","category":"sdk","summary":"Default-inert lifecycle hook exemplar for package authors.","tags":["hooks","lifecycle","sdk"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-lifecycle-hooks#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-lifecycle-hooks","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@unbrained/pm-linked-test-adapters","version":"2026.10.
|
|
1
|
+
{"name":"@unbrained/pm-linked-test-adapters","version":"2026.10.20","private":true,"type":"module","description":"First-party pm package for optional linked-test run management adapters and reporters.","homepage":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-linked-test-adapters#readme","repository":{"type":"git","url":"git+https://github.com/unbraind/pm-cli.git","directory":"packages/pm-linked-test-adapters"},"bugs":{"url":"https://github.com/unbraind/pm-cli/issues"},"keywords":["pm-package","project-management","linked-tests","background-runs","test-runs"],"pm":{"aliases":["linked-test-adapters"],"extensions":["extensions/linked-test-adapters"],"catalog":{"display_name":"Linked Test Adapters","category":"testing","summary":"Restore optional linked-test run management/reporting command surfaces.","tags":["tests","linked-tests","background","reporting"],"links":{"docs":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-linked-test-adapters#readme","repository":"https://github.com/unbraind/pm-cli/tree/main/packages/pm-linked-test-adapters","report":"https://github.com/unbraind/pm-cli/issues"}},"docs":["README.md"],"examples":["README.md"]}}
|