@unbrained/pm-cli 2026.8.10 → 2026.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +102 -80
  3. package/dist/cli/commander-usage.js +79 -7
  4. package/dist/cli/error-guidance.d.ts +10 -0
  5. package/dist/cli/error-guidance.js +76 -25
  6. package/dist/cli/main.js +6 -2
  7. package/dist/cli/register-assurance.d.ts +8 -0
  8. package/dist/cli/register-assurance.js +52 -0
  9. package/dist/cli/register-files-lookup.js +14 -3
  10. package/dist/cli/register-mutation.js +30 -9
  11. package/dist/cli/register-operations.js +16 -11
  12. package/dist/cli-bundle/bundle-manifest.json +159 -159
  13. package/dist/cli-bundle/chunks/{chunk-TRWXF476.js → chunk-2OOU2AFH.js} +2 -2
  14. package/dist/cli-bundle/chunks/chunk-2RA2YTHJ.js +33 -0
  15. package/dist/cli-bundle/chunks/chunk-A2SJGHSI.js +3 -0
  16. package/dist/cli-bundle/chunks/chunk-DSKYD4LG.js +13 -0
  17. package/dist/cli-bundle/chunks/chunk-HC3GM6YY.js +194 -0
  18. package/dist/cli-bundle/chunks/chunk-IUNRVDAI.js +2 -0
  19. package/dist/cli-bundle/chunks/chunk-W7CPZGSB.js +2 -0
  20. package/dist/cli-bundle/chunks/{chunk-EBO7OUUE.js → chunk-WMEZHCFI.js} +59 -53
  21. package/dist/cli-bundle/chunks/{chunk-ETML5C6H.js → chunk-ZU34C3LH.js} +47 -47
  22. package/dist/cli-bundle/chunks/chunk-ZXOC6HG5.js +5 -0
  23. package/dist/cli-bundle/chunks/{register-list-query-OJ6ZXKV4.js → register-list-query-HAUXVUPY.js} +8 -8
  24. package/dist/cli-bundle/chunks/register-mutation-7Y5U7ABT.js +20 -0
  25. package/dist/cli-bundle/chunks/register-operations-EQX3DLAZ.js +2 -0
  26. package/dist/cli-bundle/chunks/{register-setup-S3KT2LTH.js → register-setup-NOCOOWQY.js} +2 -2
  27. package/dist/cli-bundle/focused-chunks/{chunk-34SX654S.js → chunk-23FTGO3W.js} +2 -2
  28. package/dist/cli-bundle/focused-chunks/{chunk-2COZ67XT.js → chunk-2DL3MRTD.js} +2 -2
  29. package/dist/cli-bundle/focused-chunks/chunk-3AYKXMYX.js +2 -0
  30. package/dist/cli-bundle/focused-chunks/chunk-4AHEC74N.js +2 -0
  31. package/dist/cli-bundle/focused-chunks/{chunk-7UN6BHHO.js → chunk-4H5IA6AV.js} +2 -2
  32. package/dist/cli-bundle/focused-chunks/chunk-4SV3ZKG3.js +16 -0
  33. package/dist/cli-bundle/focused-chunks/chunk-55BIB6DV.js +2 -0
  34. package/dist/cli-bundle/focused-chunks/chunk-6JW6Z2P5.js +8 -0
  35. package/dist/cli-bundle/focused-chunks/{chunk-IW43E4XL.js → chunk-BKPIY34S.js} +2 -2
  36. package/dist/cli-bundle/focused-chunks/chunk-D4PIWE6D.js +2 -0
  37. package/dist/cli-bundle/focused-chunks/{chunk-KR5RVXLQ.js → chunk-DPO7TEFX.js} +29 -29
  38. package/dist/cli-bundle/focused-chunks/{chunk-JMVA4HKR.js → chunk-E3KCL7AY.js} +2 -2
  39. package/dist/cli-bundle/focused-chunks/chunk-ERAAFLPS.js +153 -0
  40. package/dist/cli-bundle/focused-chunks/{chunk-RIIN5V4N.js → chunk-F6E4JZKG.js} +2 -2
  41. package/dist/cli-bundle/focused-chunks/chunk-FPSYO5ZG.js +18 -0
  42. package/dist/cli-bundle/focused-chunks/{chunk-M6RPIXKC.js → chunk-J5EKAIVD.js} +2 -2
  43. package/dist/cli-bundle/focused-chunks/{chunk-2EXI2DKR.js → chunk-KDH5OX3D.js} +2 -2
  44. package/dist/cli-bundle/focused-chunks/chunk-OJ3LVVIS.js +2 -0
  45. package/dist/cli-bundle/focused-chunks/{chunk-DZGAHZZV.js → chunk-TL5M2PAY.js} +2 -2
  46. package/dist/cli-bundle/focused-chunks/{chunk-75774JWP.js → chunk-UDAZWT25.js} +2 -2
  47. package/dist/cli-bundle/focused-chunks/chunk-VABB4L5Y.js +17 -0
  48. package/dist/cli-bundle/main.js +13 -13
  49. package/dist/cli-bundle/sdk-authoring.js +1 -1
  50. package/dist/cli-bundle/sdk-contracts.js +1 -1
  51. package/dist/cli-bundle/sdk-core.js +41 -39
  52. package/dist/cli-bundle/sdk-governance.js +1 -1
  53. package/dist/cli-bundle/sdk-graph.js +1 -1
  54. package/dist/cli-bundle/sdk-merge.js +1 -1
  55. package/dist/cli-bundle/sdk-query.js +1 -1
  56. package/dist/cli-bundle/sdk-runtime.js +1 -1
  57. package/dist/cli-bundle/sdk-testing.js +1 -1
  58. package/dist/cli-bundle/sdk.js +2 -2
  59. package/dist/core/session/session-state.d.ts +47 -0
  60. package/dist/core/session/session-state.js +207 -6
  61. package/dist/core/shared/author.d.ts +7 -1
  62. package/dist/core/shared/author.js +66 -46
  63. package/dist/core/shared/errors.d.ts +8 -0
  64. package/dist/core/shared/errors.js +2 -2
  65. package/dist/sdk/agent/refusal-reachability.d.ts +40 -0
  66. package/dist/sdk/agent/refusal-reachability.js +72 -0
  67. package/dist/sdk/agent/subcommand-recovery.d.ts +40 -0
  68. package/dist/sdk/agent/subcommand-recovery.js +84 -0
  69. package/dist/sdk/cli-bootstrap.d.ts +8 -0
  70. package/dist/sdk/cli-bootstrap.js +7 -7
  71. package/dist/sdk/cli-contracts/completeness.js +13 -3
  72. package/dist/sdk/cli-contracts/enum-contracts.d.ts +1 -1
  73. package/dist/sdk/cli-contracts/enum-contracts.js +3 -2
  74. package/dist/sdk/cli-contracts/flag-contracts.d.ts +2 -0
  75. package/dist/sdk/cli-contracts/flag-contracts.js +17 -2
  76. package/dist/sdk/cli-contracts/runtime-contracts.js +4 -3
  77. package/dist/sdk/cli-contracts/schemas/assurance-definition.d.ts +513 -0
  78. package/dist/sdk/cli-contracts/schemas/assurance-definition.js +251 -0
  79. package/dist/sdk/cli-contracts/tool-parameter-tables.js +79 -2
  80. package/dist/sdk/cli-contracts/tool-schema.d.ts +2 -2
  81. package/dist/sdk/cli-contracts/tool-schema.js +34 -4
  82. package/dist/sdk/cli-contracts.d.ts +1 -1
  83. package/dist/sdk/cli-contracts.js +3 -3
  84. package/dist/sdk/completion.js +9 -3
  85. package/dist/sdk/config.js +9 -3
  86. package/dist/sdk/context/semantic-session-attribution.d.ts +32 -0
  87. package/dist/sdk/context/semantic-session-attribution.js +83 -0
  88. package/dist/sdk/core-governance.d.ts +3 -1
  89. package/dist/sdk/core-governance.js +5 -3
  90. package/dist/sdk/core.d.ts +1 -1
  91. package/dist/sdk/core.js +2 -2
  92. package/dist/sdk/error-code-catalog.d.ts +13 -0
  93. package/dist/sdk/error-code-catalog.js +35 -2
  94. package/dist/sdk/extension.js +18 -17
  95. package/dist/sdk/files.d.ts +11 -0
  96. package/dist/sdk/files.js +82 -24
  97. package/dist/sdk/generated-error-code-catalog.js +32 -13
  98. package/dist/sdk/governance/assurance-action-contracts.d.ts +7 -0
  99. package/dist/sdk/governance/assurance-action-contracts.js +5 -0
  100. package/dist/sdk/governance/assurance-action.d.ts +70 -0
  101. package/dist/sdk/governance/assurance-action.js +198 -0
  102. package/dist/sdk/governance/assurance-limits.d.ts +7 -0
  103. package/dist/sdk/governance/assurance-limits.js +11 -0
  104. package/dist/sdk/governance/assurance-runtime.d.ts +20 -0
  105. package/dist/sdk/governance/assurance-runtime.js +131 -0
  106. package/dist/sdk/governance/assurance.d.ts +432 -0
  107. package/dist/sdk/governance/assurance.js +689 -0
  108. package/dist/sdk/graph/run.js +8 -3
  109. package/dist/sdk/index.d.ts +6 -1
  110. package/dist/sdk/index.js +8 -3
  111. package/dist/sdk/lifecycle/claim.js +18 -3
  112. package/dist/sdk/lifecycle/focus.js +13 -3
  113. package/dist/sdk/lifecycle/plan.js +8 -3
  114. package/dist/sdk/provenance.d.ts +38 -0
  115. package/dist/sdk/provenance.js +64 -2
  116. package/dist/sdk/query/context.js +19 -8
  117. package/dist/sdk/runtime-extended-actions.js +22 -5
  118. package/dist/sdk/runtime-primitives.d.ts +2 -0
  119. package/dist/sdk/runtime-primitives.js +4 -2
  120. package/dist/sdk/runtime.d.ts +4 -0
  121. package/dist/sdk/runtime.js +31 -27
  122. package/dist/sdk/telemetry.js +7 -4
  123. package/dist/sdk/traceability/runtime-files-lookup.d.ts +8 -0
  124. package/dist/sdk/traceability/runtime-files-lookup.js +33 -0
  125. package/dist/sdk/traceability/source-traceability.d.ts +102 -0
  126. package/dist/sdk/traceability/source-traceability.js +293 -0
  127. package/dist/types.d.ts +8 -2
  128. package/dist/types.js +2 -2
  129. package/docs/ASSURANCE.md +132 -0
  130. package/docs/README.md +1 -0
  131. package/docs/RELEASING.md +9 -5
  132. package/docs/SDK.md +12 -1
  133. package/docs/SDK_AGENT_SESSION_CONTEXT.md +40 -4
  134. package/docs/SDK_EVIDENCE_TRACEABILITY.md +39 -1
  135. package/docs/SDK_RUNTIME_BOUNDARIES.md +36 -1
  136. package/docs/TESTING.md +12 -9
  137. package/docs/generated/AGENT_COMMAND_SURFACE.md +1 -0
  138. package/marketplace.json +2 -2
  139. package/package.json +3 -3
  140. package/packages/pm-beads/package.json +1 -1
  141. package/packages/pm-calendar/package.json +1 -1
  142. package/packages/pm-command-kit/package.json +1 -1
  143. package/packages/pm-digital-twin/package.json +1 -1
  144. package/packages/pm-governance-audit/package.json +1 -1
  145. package/packages/pm-guide-shell/package.json +1 -1
  146. package/packages/pm-kanban/package.json +1 -1
  147. package/packages/pm-lifecycle-hooks/package.json +1 -1
  148. package/packages/pm-linked-test-adapters/package.json +1 -1
  149. package/packages/pm-search-advanced/package.json +1 -1
  150. package/packages/pm-templates/extensions/templates/index.ts +9 -3
  151. package/packages/pm-templates/package.json +1 -1
  152. package/packages/pm-todos/package.json +1 -1
  153. package/packages/pm-vcs/package.json +1 -1
  154. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  155. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  156. package/sdk/public-surface.json +840 -28
  157. package/dist/cli-bundle/chunks/chunk-2QYH4QMI.js +0 -30
  158. package/dist/cli-bundle/chunks/chunk-JUDU4RO4.js +0 -194
  159. package/dist/cli-bundle/chunks/chunk-JZSNADRR.js +0 -5
  160. package/dist/cli-bundle/chunks/chunk-MENWVGJW.js +0 -2
  161. package/dist/cli-bundle/chunks/chunk-S4EITGLA.js +0 -3
  162. package/dist/cli-bundle/chunks/chunk-TIGKWVLA.js +0 -13
  163. package/dist/cli-bundle/chunks/chunk-TSLDTJLQ.js +0 -2
  164. package/dist/cli-bundle/chunks/register-mutation-DKI7QC7O.js +0 -20
  165. package/dist/cli-bundle/chunks/register-operations-P5OGMT6I.js +0 -2
  166. package/dist/cli-bundle/focused-chunks/chunk-36FTZ2NT.js +0 -2
  167. package/dist/cli-bundle/focused-chunks/chunk-3CQECMKR.js +0 -16
  168. package/dist/cli-bundle/focused-chunks/chunk-CHIBMEZR.js +0 -8
  169. package/dist/cli-bundle/focused-chunks/chunk-FF4MNAYB.js +0 -153
  170. package/dist/cli-bundle/focused-chunks/chunk-GHTN2ILR.js +0 -2
  171. package/dist/cli-bundle/focused-chunks/chunk-I4YM2I7S.js +0 -2
  172. package/dist/cli-bundle/focused-chunks/chunk-IMDTEN7K.js +0 -2
  173. package/dist/cli-bundle/focused-chunks/chunk-KSNJG4KI.js +0 -16
  174. package/dist/cli-bundle/focused-chunks/chunk-MXYSIQ56.js +0 -18
  175. package/dist/cli-bundle/focused-chunks/chunk-RKWSCYYN.js +0 -2
@@ -0,0 +1,689 @@
1
+ /**
2
+ * @module sdk/assurance
3
+ *
4
+ * Declares, persists, evaluates, and audits project assurance contracts. The
5
+ * module deliberately owns semantics while hosts inject workspace data and
6
+ * external measurement adapters, keeping CLI, MCP, CI, and embedded callers on
7
+ * one deterministic SDK path.
8
+ */
9
+
10
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2c6c336c-abaa-53f7-94ac-ba4d5ab7c76a")}catch(e){}}();
11
+ import path from "node:path";
12
+ import { EXIT_CODE } from "../../core/shared/constants.js";
13
+ import { PmCliError } from "../../core/shared/errors.js";
14
+ import { appendWorkspaceAuditEvent, mutateWorkspaceJsonWithHistory, readFileIfExists, readHistoryEntries, resolveAuthor, stableStringify, WORKSPACE_HISTORY_ID, getWorkspaceHistoryPath, } from "../runtime-primitives.js";
15
+ import { MAX_ASSURANCE_VERDICT_LIMIT } from "./assurance-limits.js";
16
+ /** Current serialized assurance registry format. */
17
+ export const ASSURANCE_DOCUMENT_VERSION = 1;
18
+ /** Supported lifecycle points that can evaluate a gate. */
19
+ export const ASSURANCE_GATE_TRIGGERS = [
20
+ "pre-commit",
21
+ "pre-push",
22
+ "pre-merge",
23
+ "ci",
24
+ "pre-release",
25
+ "post-release",
26
+ "scheduled",
27
+ "on-claim",
28
+ "on-close",
29
+ ];
30
+ const POLARITY_KEYS = [
31
+ "ceiling",
32
+ "floor",
33
+ "equals",
34
+ "zero",
35
+ "monotone_nondecreasing",
36
+ "monotone_nonincreasing",
37
+ "subset_of",
38
+ ];
39
+ const ID_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
40
+ const LOCK_TTL_SECONDS = 30;
41
+ const LOCK_WAIT_MS = 5_000;
42
+ const EVALUATION_CONCURRENCY = 4;
43
+ const DEFAULT_VERDICT_LIMIT = 50;
44
+ function requireStableId(value, field) {
45
+ if (!ID_PATTERN.test(value)) {
46
+ throw new TypeError(`${field} must be a stable lowercase id`);
47
+ }
48
+ }
49
+ function requireFiniteNonNegative(value, field) {
50
+ if (!Number.isFinite(value) || value < 0) {
51
+ throw new TypeError(`${field} must be a finite non-negative number`);
52
+ }
53
+ }
54
+ function boundFor(assertion) {
55
+ const present = POLARITY_KEYS.filter((key) => assertion[key] !== undefined);
56
+ if (present.length !== 1) {
57
+ throw new TypeError(`assertion ${assertion.id} must declare exactly one polarity`);
58
+ }
59
+ const polarity = present[0];
60
+ const raw = assertion[polarity];
61
+ return {
62
+ polarity,
63
+ value: polarity === "zero" ? 0 : raw,
64
+ };
65
+ }
66
+ function valuesEqual(left, right) {
67
+ return stableStringify(left) === stableStringify(right);
68
+ }
69
+ function compareBound(observed, bound) {
70
+ if (bound.polarity === "equals")
71
+ return valuesEqual(observed, bound.value);
72
+ if (bound.polarity === "subset_of") {
73
+ if (!Array.isArray(observed) || !Array.isArray(bound.value))
74
+ return false;
75
+ const allowed = bound.value;
76
+ return observed.every((entry) => allowed.includes(entry));
77
+ }
78
+ if (typeof observed !== "number") {
79
+ return false;
80
+ }
81
+ const numericBound = bound.value;
82
+ if (bound.polarity === "ceiling" || bound.polarity === "monotone_nonincreasing") {
83
+ return observed <= numericBound;
84
+ }
85
+ if (bound.polarity === "floor" || bound.polarity === "monotone_nondecreasing") {
86
+ return observed >= numericBound;
87
+ }
88
+ return observed === 0;
89
+ }
90
+ function proveNegativeControl(assertion) {
91
+ const bound = boundFor(assertion);
92
+ const cases = assertion.negative_control.cases;
93
+ return (cases.some((entry) => entry.expected === "pass") &&
94
+ cases.some((entry) => entry.expected === "fail") &&
95
+ cases.every((entry) => compareBound(entry.observed, bound) === (entry.expected === "pass")));
96
+ }
97
+ function boundUpdateIsLoosening(beforeBound, afterBound) {
98
+ if (beforeBound.polarity !== afterBound.polarity)
99
+ return true;
100
+ if (typeof beforeBound.value === "number" && typeof afterBound.value === "number") {
101
+ return (beforeBound.polarity === "ceiling" || beforeBound.polarity === "monotone_nonincreasing"
102
+ ? afterBound.value > beforeBound.value
103
+ : beforeBound.polarity === "floor" || beforeBound.polarity === "monotone_nondecreasing"
104
+ ? afterBound.value < beforeBound.value
105
+ : afterBound.value !== beforeBound.value);
106
+ }
107
+ if (beforeBound.polarity === "subset_of") {
108
+ const beforeValues = beforeBound.value;
109
+ const afterValues = afterBound.value;
110
+ return (!Array.isArray(beforeValues) ||
111
+ !Array.isArray(afterValues) ||
112
+ afterValues.some((entry) => !beforeValues.includes(entry)));
113
+ }
114
+ return !valuesEqual(beforeBound.value, afterBound.value);
115
+ }
116
+ /** Return true when an assertion update weakens scope, bound, lifetime, or enforcement. */
117
+ export function assuranceAssertionUpdateIsLoosening(before, after) {
118
+ if (before.measurement_id !== after.measurement_id ||
119
+ before.owner_item_id !== after.owner_item_id ||
120
+ stableStringify(before.scope) !== stableStringify(after.scope)) {
121
+ return true;
122
+ }
123
+ const enforcementRank = {
124
+ observe: 0,
125
+ warn: 1,
126
+ block: 2,
127
+ };
128
+ const lifetimeLoosened = (before.lifetime ?? "hold") === "hold" && (after.lifetime ?? "hold") === "retire";
129
+ return (boundUpdateIsLoosening(boundFor(before), boundFor(after)) ||
130
+ enforcementRank[after.enforcement] < enforcementRank[before.enforcement] ||
131
+ lifetimeLoosened);
132
+ }
133
+ /** Validate and return one measurement declaration. */
134
+ export function validateMeasurementDefinition(definition) {
135
+ requireStableId(definition.id, "measurement.id");
136
+ if (definition.max_cost !== undefined) {
137
+ requireFiniteNonNegative(definition.max_cost, "measurement.max_cost");
138
+ }
139
+ const source = definition.source;
140
+ if (source.kind === "items" && source.field !== undefined && source.equals === undefined) {
141
+ throw new TypeError("items source with field requires equals");
142
+ }
143
+ if (source.kind === "dependency_kind" && source.dependency_kind.trim().length === 0) {
144
+ throw new TypeError("dependency_kind source requires dependency_kind");
145
+ }
146
+ if ((source.kind === "graph" || source.kind === "validate" || source.kind === "health") && source.field.trim().length === 0) {
147
+ throw new TypeError(`${source.kind} source requires field`);
148
+ }
149
+ if (source.kind === "provider") {
150
+ requireStableId(source.provider, "provider source provider");
151
+ requireStableId(source.key, "provider source key");
152
+ }
153
+ return definition;
154
+ }
155
+ /** Validate and return one assertion declaration. */
156
+ export function validateAssertionDefinition(definition) {
157
+ requireStableId(definition.id, "assertion.id");
158
+ requireStableId(definition.measurement_id, "assertion.measurement_id");
159
+ if (definition.owner_item_id.trim().length === 0) {
160
+ throw new TypeError("assertion.owner_item_id is required");
161
+ }
162
+ const bound = boundFor(definition);
163
+ if (typeof bound.value === "number" && !Number.isFinite(bound.value)) {
164
+ throw new TypeError(`assertion.${bound.polarity} must be a finite number`);
165
+ }
166
+ if (definition.lifetime === "retire" && !definition.retire_reason?.trim()) {
167
+ throw new TypeError("retired assertion requires retire_reason");
168
+ }
169
+ if (definition.scope.kind === "filter") {
170
+ requireStableId(definition.scope.measurement_id, "assertion.scope.measurement_id");
171
+ }
172
+ const cases = definition.negative_control?.cases;
173
+ if (!Array.isArray(cases) || !cases.some((entry) => entry.expected === "pass") || !cases.some((entry) => entry.expected === "fail")) {
174
+ throw new TypeError("assertion negative control requires pass and fail cases");
175
+ }
176
+ if (!proveNegativeControl(definition)) {
177
+ throw new TypeError(`assertion ${definition.id} negative control does not prove its bound`);
178
+ }
179
+ return definition;
180
+ }
181
+ /** Validate and return one gate declaration. */
182
+ export function validateGateDefinition(definition) {
183
+ requireStableId(definition.id, "gate.id");
184
+ if (definition.assertion_ids.length === 0) {
185
+ throw new TypeError("gate requires at least one assertion");
186
+ }
187
+ for (const id of definition.assertion_ids)
188
+ requireStableId(id, "gate.assertion_id");
189
+ if (definition.triggers.length === 0) {
190
+ throw new TypeError("gate requires at least one trigger");
191
+ }
192
+ return definition;
193
+ }
194
+ /** Validate registry references, uniqueness, and declaration-local contracts. */
195
+ export function validateAssuranceDocument(document) {
196
+ if (document.version !== ASSURANCE_DOCUMENT_VERSION) {
197
+ throw new TypeError(`unsupported assurance document version ${String(document.version)}`);
198
+ }
199
+ const ids = new Set();
200
+ for (const definition of document.measurements) {
201
+ validateMeasurementDefinition(definition);
202
+ if (ids.has(`measurement:${definition.id}`))
203
+ throw new TypeError(`duplicate measurement ${definition.id}`);
204
+ ids.add(`measurement:${definition.id}`);
205
+ }
206
+ for (const definition of document.assertions) {
207
+ validateAssertionDefinition(definition);
208
+ if (ids.has(`assertion:${definition.id}`))
209
+ throw new TypeError(`duplicate assertion ${definition.id}`);
210
+ ids.add(`assertion:${definition.id}`);
211
+ }
212
+ for (const definition of document.gates) {
213
+ validateGateDefinition(definition);
214
+ const missing = definition.assertion_ids.filter((id) => !document.assertions.some((assertionDefinition) => assertionDefinition.id === id));
215
+ if (missing.length > 0)
216
+ throw new TypeError(`gate ${definition.id} references missing assertions: ${missing.join(", ")}`);
217
+ if (ids.has(`gate:${definition.id}`))
218
+ throw new TypeError(`duplicate gate ${definition.id}`);
219
+ ids.add(`gate:${definition.id}`);
220
+ }
221
+ const measurementIds = new Set(document.measurements.map((entry) => entry.id));
222
+ for (const [measurementId, consumers] of collectMeasurementReferences(document)) {
223
+ if (!measurementIds.has(measurementId)) {
224
+ throw new TypeError(`${consumers.join(", ")} references missing measurement ${measurementId}`);
225
+ }
226
+ }
227
+ return document;
228
+ }
229
+ function collectExpressionMeasurementReferences(expression, references) {
230
+ if ("measurement" in expression) {
231
+ references.push(expression.measurement);
232
+ return;
233
+ }
234
+ if ("operator" in expression) {
235
+ for (const operand of expression.operands) {
236
+ collectExpressionMeasurementReferences(operand, references);
237
+ }
238
+ }
239
+ }
240
+ function collectMeasurementReferences(document) {
241
+ const references = new Map();
242
+ const add = (measurementId, consumer) => {
243
+ const consumers = references.get(measurementId) ?? [];
244
+ consumers.push(consumer);
245
+ references.set(measurementId, consumers);
246
+ };
247
+ for (const assertionDefinition of document.assertions) {
248
+ add(assertionDefinition.measurement_id, `assertion ${assertionDefinition.id}`);
249
+ if (assertionDefinition.scope.kind === "filter") {
250
+ add(assertionDefinition.scope.measurement_id, `assertion ${assertionDefinition.id} scope`);
251
+ }
252
+ }
253
+ for (const measurementDefinition of document.measurements) {
254
+ if (measurementDefinition.source.kind !== "derived")
255
+ continue;
256
+ const derivedReferences = [];
257
+ collectExpressionMeasurementReferences(measurementDefinition.source.expression, derivedReferences);
258
+ for (const measurementId of derivedReferences) {
259
+ add(measurementId, `derived measurement ${measurementDefinition.id}`);
260
+ }
261
+ }
262
+ return references;
263
+ }
264
+ /** Create an empty, valid assurance registry. */
265
+ export function createEmptyAssuranceDocument() {
266
+ return { version: ASSURANCE_DOCUMENT_VERSION, measurements: [], assertions: [], gates: [] };
267
+ }
268
+ function itemsSourceResult(source, context) {
269
+ const matches = context.items.filter((item) => (source.statuses === undefined || source.statuses.includes(item.status)) &&
270
+ (source.types === undefined || source.types.includes(item.type)) &&
271
+ (source.tags === undefined ||
272
+ source.tags.every((tag) => item.tags?.includes(tag))) &&
273
+ (source.field === undefined ||
274
+ valuesEqual(item[source.field], source.equals)));
275
+ return {
276
+ value: matches.length,
277
+ population_size: context.items.length,
278
+ cost: context.items.length,
279
+ contributors: matches.map((item) => item.id),
280
+ };
281
+ }
282
+ function dependencyKindSourceResult(source, context) {
283
+ const contributors = [];
284
+ for (const item of context.items) {
285
+ for (const dependency of item.dependencies ?? []) {
286
+ if (dependency.kind === source.dependency_kind) {
287
+ contributors.push(`${item.id}->${dependency.id}`);
288
+ }
289
+ }
290
+ }
291
+ return {
292
+ value: contributors.length,
293
+ population_size: context.items.length,
294
+ cost: context.items.length,
295
+ contributors,
296
+ };
297
+ }
298
+ function historySourceResult(source, context) {
299
+ const matches = context.history.filter((entry) => (source.op === undefined || entry.op === source.op) &&
300
+ (source.author === undefined || entry.author === source.author) &&
301
+ (source.harness === undefined ||
302
+ entry.agent_harness === source.harness) &&
303
+ (source.model === undefined || entry.agent_model === source.model));
304
+ return {
305
+ value: matches.length,
306
+ population_size: context.history.length,
307
+ cost: context.history.length,
308
+ };
309
+ }
310
+ function linksSourceResult(source, context) {
311
+ const matches = context.items.filter((item) => {
312
+ const links = item[source.link];
313
+ const present = Array.isArray(links) && links.length > 0;
314
+ return source.state === "present" ? present : !present;
315
+ });
316
+ return {
317
+ value: matches.length,
318
+ population_size: context.items.length,
319
+ cost: context.items.length,
320
+ contributors: matches.map((item) => item.id),
321
+ };
322
+ }
323
+ function sourceResult(definition, context) {
324
+ const source = definition.source;
325
+ if (source.kind === "items")
326
+ return itemsSourceResult(source, context);
327
+ if (source.kind === "dependency_kind") {
328
+ return dependencyKindSourceResult(source, context);
329
+ }
330
+ if (source.kind === "history")
331
+ return historySourceResult(source, context);
332
+ if (source.kind === "links")
333
+ return linksSourceResult(source, context);
334
+ return null;
335
+ }
336
+ function sumCosts(costs, durationMs) {
337
+ return costs.reduce((total, cost) => ({
338
+ units: total.units + cost.units,
339
+ items_scanned: total.items_scanned + cost.items_scanned,
340
+ history_entries: total.history_entries + cost.history_entries,
341
+ provider_calls: total.provider_calls + cost.provider_calls,
342
+ duration_ms: durationMs,
343
+ }), { units: 0, items_scanned: 0, history_entries: 0, provider_calls: 0, duration_ms: durationMs });
344
+ }
345
+ function arithmeticValue(operator, values) {
346
+ if (values.length === 0)
347
+ throw new TypeError(`derived ${operator} requires operands`);
348
+ if (operator === "add")
349
+ return values.reduce((sum, value) => sum + value, 0);
350
+ if (operator === "subtract")
351
+ return values.slice(1).reduce((result, value) => result - value, values[0]);
352
+ if (operator === "multiply")
353
+ return values.reduce((result, value) => result * value, 1);
354
+ if (operator === "divide") {
355
+ if (values.slice(1).includes(0))
356
+ throw new TypeError("derived divide cannot divide by zero");
357
+ return values.slice(1).reduce((result, value) => result / value, values[0]);
358
+ }
359
+ return operator === "min" ? Math.min(...values) : Math.max(...values);
360
+ }
361
+ async function mapWithConcurrency(values, limit, transform) {
362
+ const results = new Array(values.length);
363
+ let nextIndex = 0;
364
+ const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
365
+ while (nextIndex < values.length) {
366
+ const index = nextIndex;
367
+ nextIndex += 1;
368
+ results[index] = await transform(values[index], index);
369
+ }
370
+ });
371
+ await Promise.all(workers);
372
+ return results;
373
+ }
374
+ async function evaluateExpression(expression, context, definitions, stack, state) {
375
+ if ("literal" in expression)
376
+ return { value: expression.literal, costs: [], population_size: 0, contributors: [] };
377
+ if ("measurement" in expression) {
378
+ const definition = definitions.find((entry) => entry.id === expression.measurement);
379
+ if (!definition)
380
+ throw new TypeError(`derived measurement references missing ${expression.measurement}`);
381
+ const result = await evaluateMeasurementCached(definition, context, definitions, stack, state);
382
+ if (typeof result.value !== "number")
383
+ throw new TypeError(`derived measurement ${definition.id} is not numeric`);
384
+ const costs = state.charged.has(definition.id) ? [] : [result.cost];
385
+ state.charged.add(definition.id);
386
+ return { value: result.value, costs, population_size: result.population_size, contributors: result.contributors };
387
+ }
388
+ const operands = await mapWithConcurrency(expression.operands, EVALUATION_CONCURRENCY, (operand) => evaluateExpression(operand, context, definitions, stack, state));
389
+ return {
390
+ value: arithmeticValue(expression.operator, operands.map((entry) => entry.value)),
391
+ costs: operands.flatMap((entry) => entry.costs),
392
+ population_size: Math.max(0, ...operands.map((entry) => entry.population_size)),
393
+ contributors: [...new Set(operands.flatMap((entry) => entry.contributors))],
394
+ };
395
+ }
396
+ async function evaluateMeasurementInternal(definition, context, definitions, stack, state) {
397
+ validateMeasurementDefinition(definition);
398
+ const startedAt = Date.now();
399
+ let result;
400
+ let nestedCosts = [];
401
+ let providerCalls = 0;
402
+ if (definition.source.kind === "derived") {
403
+ const derived = await evaluateExpression(definition.source.expression, context, definitions, [...stack, definition.id], state);
404
+ result = { value: derived.value, population_size: derived.population_size, cost: 0, contributors: derived.contributors };
405
+ nestedCosts = derived.costs;
406
+ }
407
+ else {
408
+ const builtin = sourceResult(definition, context);
409
+ if (builtin !== null) {
410
+ result = builtin;
411
+ }
412
+ else if (definition.source.kind === "graph" ||
413
+ definition.source.kind === "validate" ||
414
+ definition.source.kind === "health" ||
415
+ definition.source.kind === "provider") {
416
+ result = await context.external(definition.source);
417
+ providerCalls = 1;
418
+ }
419
+ else {
420
+ throw new TypeError(`unsupported assurance source ${definition.source.kind}`);
421
+ }
422
+ }
423
+ const ownCost = {
424
+ units: result.cost,
425
+ items_scanned: definition.source.kind === "items" || definition.source.kind === "dependency_kind" || definition.source.kind === "links" ? context.items.length : 0,
426
+ history_entries: definition.source.kind === "history" ? context.history.length : 0,
427
+ provider_calls: providerCalls,
428
+ duration_ms: Date.now() - startedAt,
429
+ };
430
+ const cost = sumCosts([...nestedCosts, ownCost], Date.now() - startedAt);
431
+ if (definition.max_cost !== undefined && cost.units > definition.max_cost) {
432
+ throw new TypeError(`measurement ${definition.id} exceeded cost ceiling ${definition.max_cost} with ${cost.units}`);
433
+ }
434
+ return { id: definition.id, value: result.value, population_size: result.population_size, cost, contributors: result.contributors ?? [] };
435
+ }
436
+ function evaluateMeasurementCached(definition, context, definitions, stack, state) {
437
+ if (stack.includes(definition.id)) {
438
+ throw new TypeError(`derived measurement cycle: ${[...stack, definition.id].join(" -> ")}`);
439
+ }
440
+ const cached = state.cache.get(definition.id);
441
+ if (cached)
442
+ return cached;
443
+ const evaluation = evaluateMeasurementInternal(definition, context, definitions, stack, state);
444
+ state.cache.set(definition.id, evaluation);
445
+ return evaluation;
446
+ }
447
+ /** Evaluate one measurement with cycle detection and a complete cost receipt. */
448
+ export async function evaluateMeasurement(definition, context, definitions = [definition]) {
449
+ return evaluateMeasurementCached(definition, context, definitions, [], {
450
+ cache: new Map(),
451
+ charged: new Set(),
452
+ });
453
+ }
454
+ function assertionDistance(observed, bound) {
455
+ if (typeof observed !== "number" || typeof bound.value !== "number")
456
+ return null;
457
+ if (bound.polarity === "ceiling" || bound.polarity === "monotone_nonincreasing")
458
+ return bound.value - observed;
459
+ if (bound.polarity === "floor" || bound.polarity === "monotone_nondecreasing")
460
+ return observed - bound.value;
461
+ return observed - bound.value;
462
+ }
463
+ /** Evaluate one assertion against a precomputed measurement receipt. */
464
+ export function evaluateAssuranceAssertion(definition, measurement, options = {}) {
465
+ validateAssertionDefinition(definition);
466
+ const bound = boundFor(definition);
467
+ const retired = definition.lifetime === "retire" && options.owner_terminal === true;
468
+ return {
469
+ assertion_id: definition.id,
470
+ measurement_id: definition.measurement_id,
471
+ scope: definition.scope,
472
+ population_size: measurement.population_size,
473
+ observed: measurement.value,
474
+ bound,
475
+ distance: assertionDistance(measurement.value, bound),
476
+ verdict: retired ? "retired" : compareBound(measurement.value, bound) ? "pass" : "fail",
477
+ enforcement: definition.enforcement,
478
+ negative_control_proven: proveNegativeControl(definition),
479
+ cost: measurement.cost,
480
+ contributors: measurement.contributors,
481
+ };
482
+ }
483
+ /** Evaluate a named gate into one presentation-independent verdict document. */
484
+ export async function evaluateAssuranceGate(gateId, document, context, options) {
485
+ validateAssuranceDocument(document);
486
+ const gateDefinition = document.gates.find((entry) => entry.id === gateId);
487
+ if (!gateDefinition)
488
+ throw new TypeError(`assurance gate ${gateId} not found`);
489
+ if (!gateDefinition.triggers.includes(options.trigger)) {
490
+ throw new TypeError(`assurance gate ${gateId} does not declare trigger ${options.trigger}`);
491
+ }
492
+ const assertions = await mapWithConcurrency(gateDefinition.assertion_ids, EVALUATION_CONCURRENCY, async (id) => {
493
+ const assertionDefinition = document.assertions.find((entry) => entry.id === id);
494
+ const measurementDefinition = document.measurements.find((entry) => entry.id === assertionDefinition.measurement_id);
495
+ let scopedContext = context;
496
+ if (assertionDefinition.scope.kind === "active") {
497
+ const terminalStatuses = new Set(context.terminal_statuses ?? ["closed", "canceled"]);
498
+ scopedContext = {
499
+ ...context,
500
+ items: context.items.filter((item) => !terminalStatuses.has(item.status)),
501
+ };
502
+ }
503
+ else if (assertionDefinition.scope.kind === "filter") {
504
+ const scopeMeasurementId = assertionDefinition.scope.measurement_id;
505
+ const scopeDefinition = document.measurements.find((entry) => entry.id === scopeMeasurementId);
506
+ const scope = await evaluateMeasurement(scopeDefinition, context, document.measurements);
507
+ const contributorIds = new Set(scope.contributors);
508
+ scopedContext = {
509
+ ...context,
510
+ items: context.items.filter((item) => contributorIds.has(item.id)),
511
+ };
512
+ }
513
+ const owner = context.items.find((item) => item.id === assertionDefinition.owner_item_id);
514
+ const ownerTerminal = owner !== undefined &&
515
+ new Set(context.terminal_statuses ?? ["closed", "canceled"]).has(owner.status);
516
+ return evaluateAssuranceAssertion(assertionDefinition, await evaluateMeasurement(measurementDefinition, scopedContext, document.measurements), { owner_terminal: ownerTerminal });
517
+ });
518
+ const blocking = assertions.some((entry) => entry.verdict === "fail" && entry.enforcement === "block");
519
+ const warning = assertions.some((entry) => entry.verdict === "fail" && entry.enforcement === "warn");
520
+ return {
521
+ gate_id: gateDefinition.id,
522
+ tree_id: context.tree_id,
523
+ trigger: options.trigger,
524
+ dry_run: options.dry_run === true,
525
+ evaluated_at: new Date().toISOString(),
526
+ verdict: blocking ? "block" : warning ? "warn" : "pass",
527
+ exit_code: blocking ? 1 : 0,
528
+ assertions,
529
+ cost: sumCosts(assertions.map((entry) => entry.cost), Math.max(0, ...assertions.map((entry) => entry.cost.duration_ms))),
530
+ };
531
+ }
532
+ function assurancePath(pmRoot) {
533
+ return path.join(pmRoot, "assurance.json");
534
+ }
535
+ async function readDocument(pmRoot) {
536
+ const raw = await readFileIfExists(assurancePath(pmRoot));
537
+ if (raw === null)
538
+ return createEmptyAssuranceDocument();
539
+ return parseAssuranceDocument(raw);
540
+ }
541
+ function parseAssuranceDocument(raw) {
542
+ try {
543
+ return validateAssuranceDocument(JSON.parse(raw));
544
+ }
545
+ catch (error) {
546
+ throw new PmCliError("The assurance registry is invalid. Repair or restore .agents/pm/assurance.json and retry.", EXIT_CODE.GENERIC_FAILURE, {
547
+ code: "assurance_registry_invalid",
548
+ reason: String(error),
549
+ nextSteps: [
550
+ "Restore a valid assurance.json from version control or repair its declaration references.",
551
+ "Run pm assurance list measurement after the repair.",
552
+ ],
553
+ });
554
+ }
555
+ }
556
+ function collectionFor(document, kind) {
557
+ if (kind === "measurement")
558
+ return document.measurements;
559
+ if (kind === "assertion")
560
+ return document.assertions;
561
+ return document.gates;
562
+ }
563
+ function validateForKind(kind, definition) {
564
+ if (kind === "measurement")
565
+ validateMeasurementDefinition(definition);
566
+ else if (kind === "assertion")
567
+ validateAssertionDefinition(definition);
568
+ else
569
+ validateGateDefinition(definition);
570
+ }
571
+ /** Read all declarations of one kind with a stable row selector receipt. */
572
+ export async function listAssuranceDeclarations(pmRoot, kind) {
573
+ const items = structuredClone(collectionFor(await readDocument(pmRoot), kind));
574
+ return { items, count: items.length, row_contract: { row_keys: ["items"], jq_selector: ".items[]" } };
575
+ }
576
+ /** Read one named declaration or fail loudly. */
577
+ export async function getAssuranceDeclaration(pmRoot, kind, id) {
578
+ const found = collectionFor(await readDocument(pmRoot), kind).find((entry) => entry.id === id);
579
+ if (!found)
580
+ throw new TypeError(`assurance ${kind} ${id} not found`);
581
+ return structuredClone(found);
582
+ }
583
+ /** Create or update one declaration through audited singleton persistence. */
584
+ export async function putAssuranceDeclaration(pmRoot, kind, definition, options = {}) {
585
+ validateForKind(kind, definition);
586
+ const author = resolveAuthor(options.author, "unknown");
587
+ const mutation = await mutateWorkspaceJsonWithHistory({
588
+ pmRoot,
589
+ filePath: assurancePath(pmRoot),
590
+ op: `assurance:${kind}:put`,
591
+ author,
592
+ message: options.message,
593
+ lockTtlSeconds: LOCK_TTL_SECONDS,
594
+ lockWaitMs: LOCK_WAIT_MS,
595
+ mutate: (beforeRaw) => {
596
+ const document = beforeRaw === null ? createEmptyAssuranceDocument() : parseAssuranceDocument(beforeRaw);
597
+ const collection = collectionFor(document, kind);
598
+ const index = collection.findIndex((entry) => entry.id === definition.id);
599
+ const action = index < 0 ? "created" : "updated";
600
+ if (kind === "assertion" &&
601
+ index >= 0 &&
602
+ assuranceAssertionUpdateIsLoosening(collection[index], definition)) {
603
+ const decision = definition.authorization_decision;
604
+ if (!decision || !options.authorized_decision_ids?.includes(decision)) {
605
+ throw new TypeError(`loosening assertion ${definition.id} requires a verified authorization_decision`);
606
+ }
607
+ }
608
+ if (index < 0)
609
+ collection.push(structuredClone(definition));
610
+ else
611
+ collection[index] = structuredClone(definition);
612
+ validateAssuranceDocument(document);
613
+ return {
614
+ raw: `${JSON.stringify(document, null, 2)}\n`,
615
+ result: { changed: true, action, kind, id: definition.id },
616
+ };
617
+ },
618
+ });
619
+ return { ...mutation.result, changed: mutation.changed };
620
+ }
621
+ /** Remove an unreferenced declaration through audited singleton persistence. */
622
+ export async function removeAssuranceDeclaration(pmRoot, kind, id, options = {}) {
623
+ const author = resolveAuthor(options.author, "unknown");
624
+ const mutation = await mutateWorkspaceJsonWithHistory({
625
+ pmRoot,
626
+ filePath: assurancePath(pmRoot),
627
+ op: `assurance:${kind}:remove`,
628
+ author,
629
+ message: options.message,
630
+ lockTtlSeconds: LOCK_TTL_SECONDS,
631
+ lockWaitMs: LOCK_WAIT_MS,
632
+ mutate: (beforeRaw) => {
633
+ const document = beforeRaw === null ? createEmptyAssuranceDocument() : parseAssuranceDocument(beforeRaw);
634
+ if (kind === "measurement") {
635
+ const consumers = collectMeasurementReferences(document).get(id);
636
+ if (consumers && consumers.length > 0) {
637
+ throw new TypeError(`assurance measurement ${id} is referenced by ${consumers.join(", ")}`);
638
+ }
639
+ }
640
+ if (kind === "assertion" && document.gates.some((entry) => entry.assertion_ids.includes(id))) {
641
+ throw new TypeError(`assurance assertion ${id} is referenced by gate`);
642
+ }
643
+ const collection = collectionFor(document, kind);
644
+ const index = collection.findIndex((entry) => entry.id === id);
645
+ if (index < 0)
646
+ throw new TypeError(`assurance ${kind} ${id} not found`);
647
+ collection.splice(index, 1);
648
+ validateAssuranceDocument(document);
649
+ return {
650
+ raw: `${JSON.stringify(document, null, 2)}\n`,
651
+ result: { changed: true, action: "removed", kind, id },
652
+ };
653
+ },
654
+ });
655
+ return { ...mutation.result, changed: mutation.changed };
656
+ }
657
+ /** Append one immutable gate verdict without changing registry state. */
658
+ export async function recordAssuranceVerdict(pmRoot, verdict, options = {}) {
659
+ if (verdict.dry_run)
660
+ throw new TypeError("dry-run assurance verdicts are not persisted");
661
+ await appendWorkspaceAuditEvent({
662
+ pmRoot,
663
+ op: "assurance:gate:verdict",
664
+ author: resolveAuthor(options.author, "unknown"),
665
+ context: { assurance_verdict: verdict },
666
+ message: options.message ?? `Assurance gate ${verdict.gate_id} returned ${verdict.verdict}`,
667
+ lockTtlSeconds: LOCK_TTL_SECONDS,
668
+ lockWaitMs: LOCK_WAIT_MS,
669
+ });
670
+ }
671
+ /** Query durable assurance verdicts from the verified workspace stream. */
672
+ export async function listAssuranceVerdicts(pmRoot, options = {}) {
673
+ const limit = options.limit ?? DEFAULT_VERDICT_LIMIT;
674
+ if (!Number.isInteger(limit) ||
675
+ limit < 1 ||
676
+ limit > MAX_ASSURANCE_VERDICT_LIMIT) {
677
+ throw new TypeError(`assurance verdict limit must be an integer from 1 through ${MAX_ASSURANCE_VERDICT_LIMIT}`);
678
+ }
679
+ const entries = await readHistoryEntries(getWorkspaceHistoryPath(pmRoot), WORKSPACE_HISTORY_ID);
680
+ return entries
681
+ .reverse()
682
+ .filter((entry) => entry.op === "assurance:gate:verdict")
683
+ .map((entry) => entry.context?.assurance_verdict)
684
+ .filter((value) => typeof value === "object" && value !== null && "gate_id" in value)
685
+ .filter((verdict) => options.gate_id === undefined || verdict.gate_id === options.gate_id)
686
+ .slice(0, limit);
687
+ }
688
+ //# sourceMappingURL=assurance.js.map
689
+ //# debugId=2c6c336c-abaa-53f7-94ac-ba4d5ab7c76a