@unbrained/pm-cli 2026.8.11 → 2026.8.13
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 +2 -2
- package/CHANGELOG.md +74 -21
- package/dist/cli/commander-usage.js +37 -11
- package/dist/cli/error-guidance.d.ts +2 -0
- package/dist/cli/error-guidance.js +82 -19
- package/dist/cli/main.js +35 -19
- package/dist/cli/register-annotations.js +56 -3
- package/dist/cli/register-assurance.js +28 -17
- package/dist/cli/register-list-query.js +10 -7
- package/dist/cli/register-operations.js +16 -6
- package/dist/cli-bundle/bundle-manifest.json +163 -163
- package/dist/cli-bundle/chunks/chunk-3ZMOWAFM.js +2 -0
- package/dist/cli-bundle/chunks/chunk-DEDU3DG7.js +196 -0
- package/dist/cli-bundle/chunks/chunk-DL5M5J3V.js +13 -0
- package/dist/cli-bundle/chunks/chunk-F7FJYGK3.js +8 -0
- package/dist/cli-bundle/chunks/chunk-HIUZVCMN.js +5 -0
- package/dist/cli-bundle/chunks/chunk-IMGIN7R6.js +2 -0
- package/dist/cli-bundle/chunks/{chunk-A2SJGHSI.js → chunk-Q47C67TQ.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-WMEZHCFI.js → chunk-QGTUPEU5.js} +62 -54
- package/dist/cli-bundle/chunks/chunk-TF3TPKPR.js +34 -0
- package/dist/cli-bundle/chunks/{chunk-ZU34C3LH.js → chunk-YLBAPY22.js} +50 -50
- package/dist/cli-bundle/chunks/register-list-query-4HIZ7EA3.js +10 -0
- package/dist/cli-bundle/chunks/register-mutation-W64FZ5HN.js +20 -0
- package/dist/cli-bundle/chunks/register-operations-C44ET7R5.js +2 -0
- package/dist/cli-bundle/chunks/{register-setup-NOCOOWQY.js → register-setup-634MFRXU.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-27N23W6M.js +31 -0
- package/dist/cli-bundle/focused-chunks/{chunk-4H5IA6AV.js → chunk-2JP7W7GO.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-2RI2FESN.js +4 -0
- package/dist/cli-bundle/focused-chunks/chunk-3BNVU6LC.js +8 -0
- package/dist/cli-bundle/focused-chunks/{chunk-2DL3MRTD.js → chunk-3DJSGWVB.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-6MLIRBLV.js +5 -0
- package/dist/cli-bundle/focused-chunks/chunk-6XYLENDV.js +155 -0
- package/dist/cli-bundle/focused-chunks/chunk-CP4K4WFS.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-TL5M2PAY.js → chunk-E3AVJ655.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-EBTNSFYE.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-J5EKAIVD.js → chunk-HC2OL2H2.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-BKPIY34S.js → chunk-I75MANL7.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-IIS2FRKL.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-NFHFZH2Z.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-OGC4XSBC.js +17 -0
- package/dist/cli-bundle/focused-chunks/{chunk-UDAZWT25.js → chunk-OOYYKQWX.js} +4 -4
- package/dist/cli-bundle/focused-chunks/{chunk-23FTGO3W.js → chunk-PKPOZXLL.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-QDZUABWL.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-QQX5TDFT.js +16 -0
- package/dist/cli-bundle/focused-chunks/{chunk-55BIB6DV.js → chunk-TEZIXMTH.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-WEH6NSL6.js +18 -0
- package/dist/cli-bundle/main.js +13 -13
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +1 -1
- package/dist/cli-bundle/sdk-core.js +42 -41
- 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 +1 -1
- 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 +2 -2
- package/dist/core/checkpoint/checkpoint-gc.js +7 -12
- package/dist/core/extensions/activation-summary-markdown.js +5 -4
- package/dist/core/extensions/activation-summary.d.ts +2 -0
- package/dist/core/extensions/activation-summary.js +6 -2
- package/dist/core/extensions/capability-usage.d.ts +1 -1
- package/dist/core/extensions/capability-usage.js +4 -3
- package/dist/core/extensions/contribution-inventory.d.ts +1 -0
- package/dist/core/extensions/contribution-inventory.js +6 -2
- package/dist/core/extensions/extension-registries.js +3 -2
- package/dist/core/extensions/extension-types.d.ts +83 -1
- package/dist/core/extensions/extension-types.js +3 -2
- package/dist/core/extensions/loader.d.ts +2 -8
- package/dist/core/extensions/loader.js +53 -144
- package/dist/core/extensions/registration-values.d.ts +43 -0
- package/dist/core/extensions/registration-values.js +210 -0
- package/dist/core/extensions/reserved-host-flags.js +3 -2
- package/dist/core/extensions/runtime-registrations.d.ts +3 -1
- package/dist/core/extensions/runtime-registrations.js +12 -2
- package/dist/core/fs/fs-utils.d.ts +2 -0
- package/dist/core/fs/fs-utils.js +7 -3
- package/dist/core/history/drift-cache.js +4 -3
- package/dist/core/history/drift-scan.js +16 -17
- package/dist/core/history/history.d.ts +8 -0
- package/dist/core/history/history.js +57 -7
- package/dist/core/history/replay.d.ts +9 -1
- package/dist/core/history/replay.js +48 -13
- package/dist/core/lock/lock-gc.js +8 -13
- package/dist/core/lock/lock.js +5 -4
- package/dist/core/output/output.d.ts +2 -0
- package/dist/core/output/output.js +46 -14
- package/dist/core/search/vector-stores.js +6 -11
- package/dist/core/shared/command-types.d.ts +2 -0
- package/dist/core/shared/command-types.js +2 -2
- package/dist/core/shared/errors.d.ts +5 -0
- package/dist/core/shared/errors.js +2 -2
- package/dist/core/store/item-metadata-cache.d.ts +1 -1
- package/dist/core/store/item-metadata-cache.js +16 -9
- package/dist/core/telemetry/runtime.d.ts +6 -0
- package/dist/core/telemetry/runtime.js +101 -30
- package/dist/sdk/agent/command-recovery.d.ts +2 -0
- package/dist/sdk/agent/command-recovery.js +10 -2
- package/dist/sdk/annotations.js +4 -4
- package/dist/sdk/cli-bootstrap.d.ts +2 -0
- package/dist/sdk/cli-bootstrap.js +6 -2
- package/dist/sdk/cli-contracts/completeness.js +10 -3
- package/dist/sdk/cli-contracts/enum-contracts.d.ts +1 -1
- package/dist/sdk/cli-contracts/flag-contracts.js +9 -3
- package/dist/sdk/cli-contracts/registration-helpers.d.ts +20 -3
- package/dist/sdk/cli-contracts/registration-helpers.js +18 -12
- package/dist/sdk/cli-contracts/schemas/assurance-definition.d.ts +40 -6
- package/dist/sdk/cli-contracts/schemas/assurance-definition.js +27 -3
- package/dist/sdk/cli-contracts/tool-option-contracts.js +3 -2
- package/dist/sdk/cli-contracts/tool-parameter-tables.js +55 -5
- package/dist/sdk/cli-contracts/tool-schema.d.ts +2 -2
- package/dist/sdk/cli-contracts/tool-schema.js +22 -4
- package/dist/sdk/cli-program.js +3 -2
- package/dist/sdk/comments.js +4 -3
- package/dist/sdk/compile-cache.js +4 -3
- package/dist/sdk/completion.js +16 -7
- package/dist/sdk/compose.d.ts +3 -1
- package/dist/sdk/compose.js +16 -2
- package/dist/sdk/context-intent-contracts.js +45 -4
- package/dist/sdk/context-signal-store.js +4 -3
- package/dist/sdk/core.d.ts +1 -0
- package/dist/sdk/core.js +3 -2
- package/dist/sdk/extension/install-runtime.js +4 -4
- package/dist/sdk/extension/managed-state.js +4 -3
- package/dist/sdk/extension-contracts.d.ts +1 -1
- package/dist/sdk/flag-invocation-contracts.js +5 -6
- package/dist/sdk/generated/generated-error-code-catalog-part-1.d.ts +8 -0
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1597 -0
- package/dist/sdk/generated/generated-error-code-catalog-part-2.d.ts +8 -0
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +1608 -0
- package/dist/sdk/generated-error-code-catalog.js +6 -3140
- package/dist/sdk/governance/assurance-action.d.ts +27 -3
- package/dist/sdk/governance/assurance-action.js +117 -31
- package/dist/sdk/governance/assurance-mutation-error.d.ts +14 -0
- package/dist/sdk/governance/assurance-mutation-error.js +58 -0
- package/dist/sdk/governance/assurance-presets.d.ts +54 -0
- package/dist/sdk/governance/assurance-presets.js +136 -0
- package/dist/sdk/governance/assurance-runtime.d.ts +4 -0
- package/dist/sdk/governance/assurance-runtime.js +118 -14
- package/dist/sdk/governance/assurance.d.ts +51 -6
- package/dist/sdk/governance/assurance.js +321 -67
- package/dist/sdk/governance/gc.js +5 -11
- package/dist/sdk/governance/health.d.ts +3 -1
- package/dist/sdk/governance/health.js +24 -7
- package/dist/sdk/governance/validate-item-reader.js +3 -3
- package/dist/sdk/governance/validate.d.ts +2 -0
- package/dist/sdk/governance/validate.js +15 -5
- package/dist/sdk/graph/durable-cache.d.ts +1 -1
- package/dist/sdk/graph/durable-cache.js +156 -29
- package/dist/sdk/graph/governance-contracts.d.ts +12 -0
- package/dist/sdk/graph/governance-contracts.js +27 -0
- package/dist/sdk/graph/governance.d.ts +44 -2
- package/dist/sdk/graph/governance.js +156 -12
- package/dist/sdk/guide.js +4 -4
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +5 -4
- package/dist/sdk/lifecycle/claim.js +10 -3
- package/dist/sdk/linked-artifacts.d.ts +2 -0
- package/dist/sdk/linked-artifacts.js +29 -4
- package/dist/sdk/merge/driver.d.ts +2 -2
- package/dist/sdk/merge/driver.js +3 -3
- package/dist/sdk/merge/install.js +5 -4
- package/dist/sdk/merge/receipts.d.ts +6 -4
- package/dist/sdk/merge/receipts.js +7 -5
- package/dist/sdk/merge/three-way.d.ts +2 -2
- package/dist/sdk/merge/three-way.js +3 -3
- package/dist/sdk/output-projection.d.ts +9 -1
- package/dist/sdk/output-projection.js +45 -9
- package/dist/sdk/query/activity.d.ts +49 -2
- package/dist/sdk/query/activity.js +185 -38
- package/dist/sdk/query/get.js +19 -2
- package/dist/sdk/read-output-contracts.d.ts +5 -1
- package/dist/sdk/read-output-contracts.js +17 -2
- package/dist/sdk/read-output-session.d.ts +2 -0
- package/dist/sdk/read-output-session.js +12 -5
- package/dist/sdk/relationship-history.js +5 -5
- package/dist/sdk/runtime-primitives.d.ts +3 -3
- package/dist/sdk/runtime-primitives.js +5 -5
- package/dist/sdk/runtime-stats-options.js +3 -2
- package/dist/sdk/runtime.js +5 -9
- package/dist/sdk/schema-migration.js +4 -4
- package/dist/sdk/stats.d.ts +36 -3
- package/dist/sdk/stats.js +55 -15
- package/dist/sdk/test/execution.d.ts +4 -0
- package/dist/sdk/test/execution.js +88 -23
- package/dist/sdk/workspace-memory.js +4 -4
- package/dist/sdk/workspace-snapshot.js +8 -14
- package/dist/sdk/workspace-transaction-gc.js +4 -9
- package/dist/sdk/workspace-transaction.js +4 -4
- package/dist/sdk/workspace.js +4 -3
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -2
- package/docs/ASSURANCE.md +170 -14
- package/docs/COMMANDS.md +29 -8
- package/docs/CONFIGURATION.md +9 -0
- package/docs/DEPENDENCY_KIND_CONTRACT.md +10 -8
- package/docs/EXTENSIONS.md +11 -12
- package/docs/MERGE_SAFETY.md +3 -1
- package/docs/OUTPUT_PROJECTION_CONTRACTS.md +26 -13
- package/docs/README.md +2 -0
- package/docs/READ_OUTPUT_CONTRACTS.md +17 -2
- package/docs/RELATIONSHIP_GRAPH.md +15 -0
- package/docs/SDK.md +17 -3
- package/docs/SDK_AGENT_SESSION_CONTEXT.md +12 -1
- package/docs/SDK_CONTEXT_EVIDENCE_CONTRACTS.md +53 -0
- package/docs/SDK_CONTEXT_INTEGRITY.md +28 -1
- package/docs/TESTING.md +24 -15
- package/docs/TRUSTWORTHY_CONTEXT_EVIDENCE.md +106 -0
- package/docs/performance/sdk-entrypoint-import-costs.md +6 -2
- package/marketplace.json +2 -2
- package/package.json +6 -6
- 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-codex/.codex-plugin/plugin.json +1 -1
- package/sdk/public-surface.json +384 -83
- package/dist/cli-bundle/chunks/chunk-2OOU2AFH.js +0 -8
- package/dist/cli-bundle/chunks/chunk-2RA2YTHJ.js +0 -33
- package/dist/cli-bundle/chunks/chunk-DSKYD4LG.js +0 -13
- package/dist/cli-bundle/chunks/chunk-HC3GM6YY.js +0 -194
- package/dist/cli-bundle/chunks/chunk-IUNRVDAI.js +0 -2
- package/dist/cli-bundle/chunks/chunk-W7CPZGSB.js +0 -2
- package/dist/cli-bundle/chunks/chunk-ZXOC6HG5.js +0 -5
- package/dist/cli-bundle/chunks/register-list-query-HAUXVUPY.js +0 -10
- package/dist/cli-bundle/chunks/register-mutation-7Y5U7ABT.js +0 -20
- package/dist/cli-bundle/chunks/register-operations-EQX3DLAZ.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-3AYKXMYX.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-4AHEC74N.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-4SV3ZKG3.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-6JW6Z2P5.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-D4PIWE6D.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-DPO7TEFX.js +0 -31
- package/dist/cli-bundle/focused-chunks/chunk-E3KCL7AY.js +0 -5
- package/dist/cli-bundle/focused-chunks/chunk-ERAAFLPS.js +0 -153
- package/dist/cli-bundle/focused-chunks/chunk-F6E4JZKG.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-FPSYO5ZG.js +0 -18
- package/dist/cli-bundle/focused-chunks/chunk-KDH5OX3D.js +0 -4
- package/dist/cli-bundle/focused-chunks/chunk-OJ3LVVIS.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-VABB4L5Y.js +0 -17
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* one deterministic SDK path.
|
|
8
8
|
*/
|
|
9
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]="
|
|
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]="e491ddd9-60e7-51fd-b0c7-4f6e86ee56f9")}catch(e){}}();
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { EXIT_CODE } from "../../core/shared/constants.js";
|
|
13
14
|
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 { appendWorkspaceAuditEvent, mutateWorkspaceJsonWithHistory, readFileIfExists, readHistoryEntries, readSettings, resolveAuthor, stableStringify, WORKSPACE_HISTORY_ID, getWorkspaceHistoryPath, } from "../runtime-primitives.js";
|
|
16
|
+
import { resolveCanonicalRelationshipKind } from "../relationships.js";
|
|
15
17
|
import { MAX_ASSURANCE_VERDICT_LIMIT } from "./assurance-limits.js";
|
|
18
|
+
import { AssuranceEvaluationRefusalError, AssuranceMutationRefusalError, } from "./assurance-mutation-error.js";
|
|
16
19
|
/** Current serialized assurance registry format. */
|
|
17
20
|
export const ASSURANCE_DOCUMENT_VERSION = 1;
|
|
18
21
|
/** Supported lifecycle points that can evaluate a gate. */
|
|
@@ -43,18 +46,18 @@ const EVALUATION_CONCURRENCY = 4;
|
|
|
43
46
|
const DEFAULT_VERDICT_LIMIT = 50;
|
|
44
47
|
function requireStableId(value, field) {
|
|
45
48
|
if (!ID_PATTERN.test(value)) {
|
|
46
|
-
throw new
|
|
49
|
+
throw new AssuranceMutationRefusalError(`${field} must be a stable lowercase id`);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
function requireFiniteNonNegative(value, field) {
|
|
50
53
|
if (!Number.isFinite(value) || value < 0) {
|
|
51
|
-
throw new
|
|
54
|
+
throw new AssuranceMutationRefusalError(`${field} must be a finite non-negative number`);
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
function boundFor(assertion) {
|
|
55
58
|
const present = POLARITY_KEYS.filter((key) => assertion[key] !== undefined);
|
|
56
59
|
if (present.length !== 1) {
|
|
57
|
-
throw new
|
|
60
|
+
throw new AssuranceMutationRefusalError(`assertion ${assertion.id} must declare exactly one polarity`);
|
|
58
61
|
}
|
|
59
62
|
const polarity = present[0];
|
|
60
63
|
const raw = assertion[polarity];
|
|
@@ -79,10 +82,12 @@ function compareBound(observed, bound) {
|
|
|
79
82
|
return false;
|
|
80
83
|
}
|
|
81
84
|
const numericBound = bound.value;
|
|
82
|
-
if (bound.polarity === "ceiling" ||
|
|
85
|
+
if (bound.polarity === "ceiling" ||
|
|
86
|
+
bound.polarity === "monotone_nonincreasing") {
|
|
83
87
|
return observed <= numericBound;
|
|
84
88
|
}
|
|
85
|
-
if (bound.polarity === "floor" ||
|
|
89
|
+
if (bound.polarity === "floor" ||
|
|
90
|
+
bound.polarity === "monotone_nondecreasing") {
|
|
86
91
|
return observed >= numericBound;
|
|
87
92
|
}
|
|
88
93
|
return observed === 0;
|
|
@@ -97,12 +102,15 @@ function proveNegativeControl(assertion) {
|
|
|
97
102
|
function boundUpdateIsLoosening(beforeBound, afterBound) {
|
|
98
103
|
if (beforeBound.polarity !== afterBound.polarity)
|
|
99
104
|
return true;
|
|
100
|
-
if (typeof beforeBound.value === "number" &&
|
|
101
|
-
|
|
105
|
+
if (typeof beforeBound.value === "number" &&
|
|
106
|
+
typeof afterBound.value === "number") {
|
|
107
|
+
return beforeBound.polarity === "ceiling" ||
|
|
108
|
+
beforeBound.polarity === "monotone_nonincreasing"
|
|
102
109
|
? afterBound.value > beforeBound.value
|
|
103
|
-
: beforeBound.polarity === "floor" ||
|
|
110
|
+
: beforeBound.polarity === "floor" ||
|
|
111
|
+
beforeBound.polarity === "monotone_nondecreasing"
|
|
104
112
|
? afterBound.value < beforeBound.value
|
|
105
|
-
: afterBound.value !== beforeBound.value
|
|
113
|
+
: afterBound.value !== beforeBound.value;
|
|
106
114
|
}
|
|
107
115
|
if (beforeBound.polarity === "subset_of") {
|
|
108
116
|
const beforeValues = beforeBound.value;
|
|
@@ -125,11 +133,28 @@ export function assuranceAssertionUpdateIsLoosening(before, after) {
|
|
|
125
133
|
warn: 1,
|
|
126
134
|
block: 2,
|
|
127
135
|
};
|
|
128
|
-
const lifetimeLoosened = (before.lifetime ?? "hold") === "hold" &&
|
|
136
|
+
const lifetimeLoosened = (before.lifetime ?? "hold") === "hold" &&
|
|
137
|
+
(after.lifetime ?? "hold") === "retire";
|
|
129
138
|
return (boundUpdateIsLoosening(boundFor(before), boundFor(after)) ||
|
|
130
139
|
enforcementRank[after.enforcement] < enforcementRank[before.enforcement] ||
|
|
131
140
|
lifetimeLoosened);
|
|
132
141
|
}
|
|
142
|
+
/** Validate the field predicate contract of an item-backed measurement source. */
|
|
143
|
+
function validateItemsMeasurementSource(source) {
|
|
144
|
+
if (source.state !== undefined &&
|
|
145
|
+
!(source.state === "present" || source.state === "missing")) {
|
|
146
|
+
throw new AssuranceMutationRefusalError("items source state must be present or missing");
|
|
147
|
+
}
|
|
148
|
+
const hasEquals = Object.hasOwn(source, "equals");
|
|
149
|
+
const hasState = source.state !== undefined;
|
|
150
|
+
const predicateCount = Number(hasEquals) + Number(hasState);
|
|
151
|
+
if (source.field !== undefined && predicateCount !== 1) {
|
|
152
|
+
throw new AssuranceMutationRefusalError("items source with field requires exactly one of equals or state");
|
|
153
|
+
}
|
|
154
|
+
if (source.field === undefined && predicateCount !== 0) {
|
|
155
|
+
throw new AssuranceMutationRefusalError("items source equals/state requires field");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
133
158
|
/** Validate and return one measurement declaration. */
|
|
134
159
|
export function validateMeasurementDefinition(definition) {
|
|
135
160
|
requireStableId(definition.id, "measurement.id");
|
|
@@ -137,14 +162,19 @@ export function validateMeasurementDefinition(definition) {
|
|
|
137
162
|
requireFiniteNonNegative(definition.max_cost, "measurement.max_cost");
|
|
138
163
|
}
|
|
139
164
|
const source = definition.source;
|
|
140
|
-
if (source.kind === "items"
|
|
141
|
-
|
|
165
|
+
if (source.kind === "items") {
|
|
166
|
+
validateItemsMeasurementSource(source);
|
|
142
167
|
}
|
|
143
|
-
if (source.kind === "dependency_kind" &&
|
|
144
|
-
|
|
168
|
+
if (source.kind === "dependency_kind" &&
|
|
169
|
+
(typeof source.dependency_kind !== "string" ||
|
|
170
|
+
source.dependency_kind.trim().length === 0)) {
|
|
171
|
+
throw new AssuranceMutationRefusalError("dependency_kind source requires dependency_kind");
|
|
145
172
|
}
|
|
146
|
-
if ((source.kind === "graph" ||
|
|
147
|
-
|
|
173
|
+
if ((source.kind === "graph" ||
|
|
174
|
+
source.kind === "validate" ||
|
|
175
|
+
source.kind === "health") &&
|
|
176
|
+
(typeof source.field !== "string" || source.field.trim().length === 0)) {
|
|
177
|
+
throw new AssuranceMutationRefusalError(`${source.kind} source requires field`);
|
|
148
178
|
}
|
|
149
179
|
if (source.kind === "provider") {
|
|
150
180
|
requireStableId(source.provider, "provider source provider");
|
|
@@ -156,72 +186,108 @@ export function validateMeasurementDefinition(definition) {
|
|
|
156
186
|
export function validateAssertionDefinition(definition) {
|
|
157
187
|
requireStableId(definition.id, "assertion.id");
|
|
158
188
|
requireStableId(definition.measurement_id, "assertion.measurement_id");
|
|
159
|
-
if (definition.owner_item_id
|
|
160
|
-
|
|
189
|
+
if (typeof definition.owner_item_id !== "string" ||
|
|
190
|
+
definition.owner_item_id.trim().length === 0) {
|
|
191
|
+
throw new AssuranceMutationRefusalError("assertion.owner_item_id is required");
|
|
161
192
|
}
|
|
162
193
|
const bound = boundFor(definition);
|
|
163
194
|
if (typeof bound.value === "number" && !Number.isFinite(bound.value)) {
|
|
164
|
-
throw new
|
|
195
|
+
throw new AssuranceMutationRefusalError(`assertion.${bound.polarity} must be a finite number`);
|
|
165
196
|
}
|
|
166
|
-
if (definition.lifetime === "retire" &&
|
|
167
|
-
|
|
197
|
+
if (definition.lifetime === "retire" &&
|
|
198
|
+
(typeof definition.retire_reason !== "string" ||
|
|
199
|
+
definition.retire_reason.trim().length === 0)) {
|
|
200
|
+
throw new AssuranceMutationRefusalError("retired assertion requires retire_reason");
|
|
168
201
|
}
|
|
169
202
|
if (definition.scope.kind === "filter") {
|
|
170
203
|
requireStableId(definition.scope.measurement_id, "assertion.scope.measurement_id");
|
|
171
204
|
}
|
|
172
205
|
const cases = definition.negative_control?.cases;
|
|
173
|
-
if (!Array.isArray(cases) ||
|
|
174
|
-
|
|
206
|
+
if (!Array.isArray(cases) ||
|
|
207
|
+
!cases.some((entry) => entry.expected === "pass") ||
|
|
208
|
+
!cases.some((entry) => entry.expected === "fail")) {
|
|
209
|
+
throw new AssuranceMutationRefusalError("assertion negative control requires pass and fail cases");
|
|
175
210
|
}
|
|
176
211
|
if (!proveNegativeControl(definition)) {
|
|
177
|
-
throw new
|
|
212
|
+
throw new AssuranceMutationRefusalError(`assertion ${definition.id} negative control does not prove its bound`);
|
|
178
213
|
}
|
|
179
214
|
return definition;
|
|
180
215
|
}
|
|
216
|
+
function validateGateProviderPolicy(policy) {
|
|
217
|
+
if (!Array.isArray(policy.allowed_providers)) {
|
|
218
|
+
throw new AssuranceMutationRefusalError("gate provider_policy.allowed_providers must be an array");
|
|
219
|
+
}
|
|
220
|
+
for (const provider of policy.allowed_providers) {
|
|
221
|
+
requireStableId(provider, "gate.provider_policy.allowed_provider");
|
|
222
|
+
}
|
|
223
|
+
if (typeof policy.triggers !== "object" ||
|
|
224
|
+
policy.triggers === null ||
|
|
225
|
+
Array.isArray(policy.triggers)) {
|
|
226
|
+
throw new AssuranceMutationRefusalError("gate provider_policy.triggers must be an object");
|
|
227
|
+
}
|
|
228
|
+
for (const [trigger, rule] of Object.entries(policy.triggers)) {
|
|
229
|
+
if (!ASSURANCE_GATE_TRIGGERS.includes(trigger)) {
|
|
230
|
+
throw new AssuranceMutationRefusalError(`gate provider policy has unknown trigger ${trigger}`);
|
|
231
|
+
}
|
|
232
|
+
if (typeof rule !== "object" ||
|
|
233
|
+
rule === null ||
|
|
234
|
+
!["low", "medium", "high"].includes(rule.max_cost_class) ||
|
|
235
|
+
typeof rule.allow_network !== "boolean") {
|
|
236
|
+
throw new AssuranceMutationRefusalError(`gate provider policy trigger ${trigger} is invalid`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
181
240
|
/** Validate and return one gate declaration. */
|
|
182
241
|
export function validateGateDefinition(definition) {
|
|
183
242
|
requireStableId(definition.id, "gate.id");
|
|
184
243
|
if (definition.assertion_ids.length === 0) {
|
|
185
|
-
throw new
|
|
244
|
+
throw new AssuranceMutationRefusalError("gate requires at least one assertion");
|
|
186
245
|
}
|
|
187
246
|
for (const id of definition.assertion_ids)
|
|
188
247
|
requireStableId(id, "gate.assertion_id");
|
|
189
248
|
if (definition.triggers.length === 0) {
|
|
190
|
-
throw new
|
|
249
|
+
throw new AssuranceMutationRefusalError("gate requires at least one trigger");
|
|
250
|
+
}
|
|
251
|
+
if (definition.provider_policy) {
|
|
252
|
+
validateGateProviderPolicy(definition.provider_policy);
|
|
191
253
|
}
|
|
192
254
|
return definition;
|
|
193
255
|
}
|
|
194
256
|
/** Validate registry references, uniqueness, and declaration-local contracts. */
|
|
195
257
|
export function validateAssuranceDocument(document) {
|
|
196
258
|
if (document.version !== ASSURANCE_DOCUMENT_VERSION) {
|
|
197
|
-
throw new
|
|
259
|
+
throw new AssuranceMutationRefusalError(`unsupported assurance document version ${String(document.version)}`);
|
|
198
260
|
}
|
|
199
261
|
const ids = new Set();
|
|
200
262
|
for (const definition of document.measurements) {
|
|
201
263
|
validateMeasurementDefinition(definition);
|
|
202
|
-
if (ids.has(`measurement:${definition.id}`))
|
|
203
|
-
throw new
|
|
264
|
+
if (ids.has(`measurement:${definition.id}`)) {
|
|
265
|
+
throw new AssuranceMutationRefusalError(`duplicate measurement ${definition.id}`);
|
|
266
|
+
}
|
|
204
267
|
ids.add(`measurement:${definition.id}`);
|
|
205
268
|
}
|
|
206
269
|
for (const definition of document.assertions) {
|
|
207
270
|
validateAssertionDefinition(definition);
|
|
208
|
-
if (ids.has(`assertion:${definition.id}`))
|
|
209
|
-
throw new
|
|
271
|
+
if (ids.has(`assertion:${definition.id}`)) {
|
|
272
|
+
throw new AssuranceMutationRefusalError(`duplicate assertion ${definition.id}`);
|
|
273
|
+
}
|
|
210
274
|
ids.add(`assertion:${definition.id}`);
|
|
211
275
|
}
|
|
212
276
|
for (const definition of document.gates) {
|
|
213
277
|
validateGateDefinition(definition);
|
|
214
278
|
const missing = definition.assertion_ids.filter((id) => !document.assertions.some((assertionDefinition) => assertionDefinition.id === id));
|
|
215
|
-
if (missing.length > 0)
|
|
216
|
-
throw new
|
|
217
|
-
|
|
218
|
-
|
|
279
|
+
if (missing.length > 0) {
|
|
280
|
+
throw new AssuranceMutationRefusalError(`gate ${definition.id} references missing assertions: ${missing.join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
if (ids.has(`gate:${definition.id}`)) {
|
|
283
|
+
throw new AssuranceMutationRefusalError(`duplicate gate ${definition.id}`);
|
|
284
|
+
}
|
|
219
285
|
ids.add(`gate:${definition.id}`);
|
|
220
286
|
}
|
|
221
287
|
const measurementIds = new Set(document.measurements.map((entry) => entry.id));
|
|
222
288
|
for (const [measurementId, consumers] of collectMeasurementReferences(document)) {
|
|
223
289
|
if (!measurementIds.has(measurementId)) {
|
|
224
|
-
throw new
|
|
290
|
+
throw new AssuranceMutationRefusalError(`${consumers.join(", ")} references missing measurement ${measurementId}`);
|
|
225
291
|
}
|
|
226
292
|
}
|
|
227
293
|
return document;
|
|
@@ -261,17 +327,60 @@ function collectMeasurementReferences(document) {
|
|
|
261
327
|
}
|
|
262
328
|
return references;
|
|
263
329
|
}
|
|
330
|
+
function collectGateProviderIds(gate, document) {
|
|
331
|
+
const pending = gate.assertion_ids.flatMap((assertionId) => {
|
|
332
|
+
const assertion = document.assertions.find((entry) => entry.id === assertionId);
|
|
333
|
+
return [
|
|
334
|
+
assertion.measurement_id,
|
|
335
|
+
...(assertion.scope.kind === "filter"
|
|
336
|
+
? [assertion.scope.measurement_id]
|
|
337
|
+
: []),
|
|
338
|
+
];
|
|
339
|
+
});
|
|
340
|
+
const visited = new Set();
|
|
341
|
+
const providers = new Set();
|
|
342
|
+
while (pending.length > 0) {
|
|
343
|
+
const measurementId = pending.pop();
|
|
344
|
+
if (visited.has(measurementId))
|
|
345
|
+
continue;
|
|
346
|
+
visited.add(measurementId);
|
|
347
|
+
const measurement = document.measurements.find((entry) => entry.id === measurementId);
|
|
348
|
+
if (measurement.source.kind === "provider") {
|
|
349
|
+
providers.add(measurement.source.provider);
|
|
350
|
+
}
|
|
351
|
+
else if (measurement.source.kind === "derived") {
|
|
352
|
+
collectExpressionMeasurementReferences(measurement.source.expression, pending);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return providers;
|
|
356
|
+
}
|
|
264
357
|
/** Create an empty, valid assurance registry. */
|
|
265
358
|
export function createEmptyAssuranceDocument() {
|
|
266
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
version: ASSURANCE_DOCUMENT_VERSION,
|
|
361
|
+
measurements: [],
|
|
362
|
+
assertions: [],
|
|
363
|
+
gates: [],
|
|
364
|
+
};
|
|
267
365
|
}
|
|
268
366
|
function itemsSourceResult(source, context) {
|
|
269
|
-
const matches = context.items.filter((item) => (source.statuses === undefined ||
|
|
367
|
+
const matches = context.items.filter((item) => (source.statuses === undefined ||
|
|
368
|
+
source.statuses.includes(item.status)) &&
|
|
270
369
|
(source.types === undefined || source.types.includes(item.type)) &&
|
|
271
370
|
(source.tags === undefined ||
|
|
272
371
|
source.tags.every((tag) => item.tags?.includes(tag))) &&
|
|
273
372
|
(source.field === undefined ||
|
|
274
|
-
|
|
373
|
+
(() => {
|
|
374
|
+
const value = item[source.field];
|
|
375
|
+
if (source.state !== undefined) {
|
|
376
|
+
const missing = value === undefined ||
|
|
377
|
+
value === null ||
|
|
378
|
+
value === "" ||
|
|
379
|
+
(Array.isArray(value) && value.length === 0);
|
|
380
|
+
return source.state === "missing" ? missing : !missing;
|
|
381
|
+
}
|
|
382
|
+
return valuesEqual(value, source.equals);
|
|
383
|
+
})()));
|
|
275
384
|
return {
|
|
276
385
|
value: matches.length,
|
|
277
386
|
population_size: context.items.length,
|
|
@@ -281,9 +390,13 @@ function itemsSourceResult(source, context) {
|
|
|
281
390
|
}
|
|
282
391
|
function dependencyKindSourceResult(source, context) {
|
|
283
392
|
const contributors = [];
|
|
393
|
+
const requestedKind = resolveCanonicalRelationshipKind(source.dependency_kind) ??
|
|
394
|
+
source.dependency_kind.trim().toLowerCase();
|
|
284
395
|
for (const item of context.items) {
|
|
285
396
|
for (const dependency of item.dependencies ?? []) {
|
|
286
|
-
|
|
397
|
+
const dependencyKind = resolveCanonicalRelationshipKind(dependency.kind) ??
|
|
398
|
+
dependency.kind.trim().toLowerCase();
|
|
399
|
+
if (dependencyKind === requestedKind) {
|
|
287
400
|
contributors.push(`${item.id}->${dependency.id}`);
|
|
288
401
|
}
|
|
289
402
|
}
|
|
@@ -340,7 +453,13 @@ function sumCosts(costs, durationMs) {
|
|
|
340
453
|
history_entries: total.history_entries + cost.history_entries,
|
|
341
454
|
provider_calls: total.provider_calls + cost.provider_calls,
|
|
342
455
|
duration_ms: durationMs,
|
|
343
|
-
}), {
|
|
456
|
+
}), {
|
|
457
|
+
units: 0,
|
|
458
|
+
items_scanned: 0,
|
|
459
|
+
history_entries: 0,
|
|
460
|
+
provider_calls: 0,
|
|
461
|
+
duration_ms: durationMs,
|
|
462
|
+
});
|
|
344
463
|
}
|
|
345
464
|
function arithmeticValue(operator, values) {
|
|
346
465
|
if (values.length === 0)
|
|
@@ -373,7 +492,12 @@ async function mapWithConcurrency(values, limit, transform) {
|
|
|
373
492
|
}
|
|
374
493
|
async function evaluateExpression(expression, context, definitions, stack, state) {
|
|
375
494
|
if ("literal" in expression)
|
|
376
|
-
return {
|
|
495
|
+
return {
|
|
496
|
+
value: expression.literal,
|
|
497
|
+
costs: [],
|
|
498
|
+
population_size: 0,
|
|
499
|
+
contributors: [],
|
|
500
|
+
};
|
|
377
501
|
if ("measurement" in expression) {
|
|
378
502
|
const definition = definitions.find((entry) => entry.id === expression.measurement);
|
|
379
503
|
if (!definition)
|
|
@@ -383,7 +507,12 @@ async function evaluateExpression(expression, context, definitions, stack, state
|
|
|
383
507
|
throw new TypeError(`derived measurement ${definition.id} is not numeric`);
|
|
384
508
|
const costs = state.charged.has(definition.id) ? [] : [result.cost];
|
|
385
509
|
state.charged.add(definition.id);
|
|
386
|
-
return {
|
|
510
|
+
return {
|
|
511
|
+
value: result.value,
|
|
512
|
+
costs,
|
|
513
|
+
population_size: result.population_size,
|
|
514
|
+
contributors: result.contributors,
|
|
515
|
+
};
|
|
387
516
|
}
|
|
388
517
|
const operands = await mapWithConcurrency(expression.operands, EVALUATION_CONCURRENCY, (operand) => evaluateExpression(operand, context, definitions, stack, state));
|
|
389
518
|
return {
|
|
@@ -401,7 +530,12 @@ async function evaluateMeasurementInternal(definition, context, definitions, sta
|
|
|
401
530
|
let providerCalls = 0;
|
|
402
531
|
if (definition.source.kind === "derived") {
|
|
403
532
|
const derived = await evaluateExpression(definition.source.expression, context, definitions, [...stack, definition.id], state);
|
|
404
|
-
result = {
|
|
533
|
+
result = {
|
|
534
|
+
value: derived.value,
|
|
535
|
+
population_size: derived.population_size,
|
|
536
|
+
cost: 0,
|
|
537
|
+
contributors: derived.contributors,
|
|
538
|
+
};
|
|
405
539
|
nestedCosts = derived.costs;
|
|
406
540
|
}
|
|
407
541
|
else {
|
|
@@ -422,16 +556,29 @@ async function evaluateMeasurementInternal(definition, context, definitions, sta
|
|
|
422
556
|
}
|
|
423
557
|
const ownCost = {
|
|
424
558
|
units: result.cost,
|
|
425
|
-
items_scanned: definition.source.kind === "items" ||
|
|
559
|
+
items_scanned: definition.source.kind === "items" ||
|
|
560
|
+
definition.source.kind === "dependency_kind" ||
|
|
561
|
+
definition.source.kind === "links"
|
|
562
|
+
? context.items.length
|
|
563
|
+
: 0,
|
|
426
564
|
history_entries: definition.source.kind === "history" ? context.history.length : 0,
|
|
427
565
|
provider_calls: providerCalls,
|
|
428
566
|
duration_ms: Date.now() - startedAt,
|
|
429
567
|
};
|
|
430
568
|
const cost = sumCosts([...nestedCosts, ownCost], Date.now() - startedAt);
|
|
431
569
|
if (definition.max_cost !== undefined && cost.units > definition.max_cost) {
|
|
432
|
-
throw new
|
|
570
|
+
throw new AssuranceEvaluationRefusalError(`measurement ${definition.id} exceeded cost ceiling ${definition.max_cost} with ${cost.units}`);
|
|
433
571
|
}
|
|
434
|
-
return {
|
|
572
|
+
return {
|
|
573
|
+
id: definition.id,
|
|
574
|
+
definition_fingerprint: `sha256:${createHash("sha256")
|
|
575
|
+
.update(stableStringify(definition))
|
|
576
|
+
.digest("hex")}`,
|
|
577
|
+
value: result.value,
|
|
578
|
+
population_size: result.population_size,
|
|
579
|
+
cost,
|
|
580
|
+
contributors: result.contributors ?? [],
|
|
581
|
+
};
|
|
435
582
|
}
|
|
436
583
|
function evaluateMeasurementCached(definition, context, definitions, stack, state) {
|
|
437
584
|
if (stack.includes(definition.id)) {
|
|
@@ -454,7 +601,8 @@ export async function evaluateMeasurement(definition, context, definitions = [de
|
|
|
454
601
|
function assertionDistance(observed, bound) {
|
|
455
602
|
if (typeof observed !== "number" || typeof bound.value !== "number")
|
|
456
603
|
return null;
|
|
457
|
-
if (bound.polarity === "ceiling" ||
|
|
604
|
+
if (bound.polarity === "ceiling" ||
|
|
605
|
+
bound.polarity === "monotone_nonincreasing")
|
|
458
606
|
return bound.value - observed;
|
|
459
607
|
if (bound.polarity === "floor" || bound.polarity === "monotone_nondecreasing")
|
|
460
608
|
return observed - bound.value;
|
|
@@ -468,18 +616,54 @@ export function evaluateAssuranceAssertion(definition, measurement, options = {}
|
|
|
468
616
|
return {
|
|
469
617
|
assertion_id: definition.id,
|
|
470
618
|
measurement_id: definition.measurement_id,
|
|
619
|
+
measurement_definition_fingerprint: measurement.definition_fingerprint,
|
|
471
620
|
scope: definition.scope,
|
|
472
621
|
population_size: measurement.population_size,
|
|
473
622
|
observed: measurement.value,
|
|
474
623
|
bound,
|
|
475
624
|
distance: assertionDistance(measurement.value, bound),
|
|
476
|
-
verdict: retired
|
|
625
|
+
verdict: retired
|
|
626
|
+
? "retired"
|
|
627
|
+
: compareBound(measurement.value, bound)
|
|
628
|
+
? "pass"
|
|
629
|
+
: "fail",
|
|
477
630
|
enforcement: definition.enforcement,
|
|
478
631
|
negative_control_proven: proveNegativeControl(definition),
|
|
479
632
|
cost: measurement.cost,
|
|
480
633
|
contributors: measurement.contributors,
|
|
481
634
|
};
|
|
482
635
|
}
|
|
636
|
+
function enforceGateProviderPolicy(gate, document, context, trigger) {
|
|
637
|
+
const providerIds = collectGateProviderIds(gate, document);
|
|
638
|
+
if (providerIds.size === 0)
|
|
639
|
+
return;
|
|
640
|
+
const policy = gate.provider_policy;
|
|
641
|
+
const triggerPolicy = policy?.triggers[trigger];
|
|
642
|
+
if (!policy || !triggerPolicy) {
|
|
643
|
+
throw new AssuranceEvaluationRefusalError(`assurance gate ${gate.id} refuses provider execution at trigger ${trigger} without an explicit provider policy`);
|
|
644
|
+
}
|
|
645
|
+
const allowedProviders = new Set(policy.allowed_providers);
|
|
646
|
+
const costRank = {
|
|
647
|
+
low: 0,
|
|
648
|
+
medium: 1,
|
|
649
|
+
high: 2,
|
|
650
|
+
};
|
|
651
|
+
for (const providerId of providerIds) {
|
|
652
|
+
if (!allowedProviders.has(providerId)) {
|
|
653
|
+
throw new AssuranceEvaluationRefusalError(`assurance gate ${gate.id} refuses provider ${providerId}`);
|
|
654
|
+
}
|
|
655
|
+
const capability = context.provider_capabilities?.[providerId];
|
|
656
|
+
if (!capability) {
|
|
657
|
+
throw new AssuranceEvaluationRefusalError(`assurance provider ${providerId} has no declared capabilities`);
|
|
658
|
+
}
|
|
659
|
+
if (costRank[capability.cost_class] > costRank[triggerPolicy.max_cost_class]) {
|
|
660
|
+
throw new AssuranceEvaluationRefusalError(`assurance gate ${gate.id} refuses ${capability.cost_class}-cost provider ${providerId} at trigger ${trigger}`);
|
|
661
|
+
}
|
|
662
|
+
if (capability.network && !triggerPolicy.allow_network) {
|
|
663
|
+
throw new AssuranceEvaluationRefusalError(`assurance gate ${gate.id} refuses network provider ${providerId} at trigger ${trigger}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
483
667
|
/** Evaluate a named gate into one presentation-independent verdict document. */
|
|
484
668
|
export async function evaluateAssuranceGate(gateId, document, context, options) {
|
|
485
669
|
validateAssuranceDocument(document);
|
|
@@ -489,6 +673,7 @@ export async function evaluateAssuranceGate(gateId, document, context, options)
|
|
|
489
673
|
if (!gateDefinition.triggers.includes(options.trigger)) {
|
|
490
674
|
throw new TypeError(`assurance gate ${gateId} does not declare trigger ${options.trigger}`);
|
|
491
675
|
}
|
|
676
|
+
enforceGateProviderPolicy(gateDefinition, document, context, options.trigger);
|
|
492
677
|
const assertions = await mapWithConcurrency(gateDefinition.assertion_ids, EVALUATION_CONCURRENCY, async (id) => {
|
|
493
678
|
const assertionDefinition = document.assertions.find((entry) => entry.id === id);
|
|
494
679
|
const measurementDefinition = document.measurements.find((entry) => entry.id === assertionDefinition.measurement_id);
|
|
@@ -571,7 +756,11 @@ function validateForKind(kind, definition) {
|
|
|
571
756
|
/** Read all declarations of one kind with a stable row selector receipt. */
|
|
572
757
|
export async function listAssuranceDeclarations(pmRoot, kind) {
|
|
573
758
|
const items = structuredClone(collectionFor(await readDocument(pmRoot), kind));
|
|
574
|
-
return {
|
|
759
|
+
return {
|
|
760
|
+
items,
|
|
761
|
+
count: items.length,
|
|
762
|
+
row_contract: { row_keys: ["items"], jq_selector: ".items[]" },
|
|
763
|
+
};
|
|
575
764
|
}
|
|
576
765
|
/** Read one named declaration or fail loudly. */
|
|
577
766
|
export async function getAssuranceDeclaration(pmRoot, kind, id) {
|
|
@@ -583,7 +772,8 @@ export async function getAssuranceDeclaration(pmRoot, kind, id) {
|
|
|
583
772
|
/** Create or update one declaration through audited singleton persistence. */
|
|
584
773
|
export async function putAssuranceDeclaration(pmRoot, kind, definition, options = {}) {
|
|
585
774
|
validateForKind(kind, definition);
|
|
586
|
-
const
|
|
775
|
+
const settings = await readSettings(pmRoot);
|
|
776
|
+
const author = resolveAuthor(options.author, settings.author_default);
|
|
587
777
|
const mutation = await mutateWorkspaceJsonWithHistory({
|
|
588
778
|
pmRoot,
|
|
589
779
|
filePath: assurancePath(pmRoot),
|
|
@@ -593,16 +783,20 @@ export async function putAssuranceDeclaration(pmRoot, kind, definition, options
|
|
|
593
783
|
lockTtlSeconds: LOCK_TTL_SECONDS,
|
|
594
784
|
lockWaitMs: LOCK_WAIT_MS,
|
|
595
785
|
mutate: (beforeRaw) => {
|
|
596
|
-
const document = beforeRaw === null
|
|
786
|
+
const document = beforeRaw === null
|
|
787
|
+
? createEmptyAssuranceDocument()
|
|
788
|
+
: parseAssuranceDocument(beforeRaw);
|
|
597
789
|
const collection = collectionFor(document, kind);
|
|
598
790
|
const index = collection.findIndex((entry) => entry.id === definition.id);
|
|
599
791
|
const action = index < 0 ? "created" : "updated";
|
|
600
792
|
if (kind === "assertion" &&
|
|
601
793
|
index >= 0 &&
|
|
602
794
|
assuranceAssertionUpdateIsLoosening(collection[index], definition)) {
|
|
603
|
-
const decision = definition
|
|
604
|
-
|
|
605
|
-
|
|
795
|
+
const decision = definition
|
|
796
|
+
.authorization_decision;
|
|
797
|
+
if (!decision ||
|
|
798
|
+
!options.authorized_decision_ids?.includes(decision)) {
|
|
799
|
+
throw new AssuranceMutationRefusalError(`loosening assertion ${definition.id} requires a verified authorization_decision`);
|
|
606
800
|
}
|
|
607
801
|
}
|
|
608
802
|
if (index < 0)
|
|
@@ -618,9 +812,63 @@ export async function putAssuranceDeclaration(pmRoot, kind, definition, options
|
|
|
618
812
|
});
|
|
619
813
|
return { ...mutation.result, changed: mutation.changed };
|
|
620
814
|
}
|
|
815
|
+
/** Apply a declaration bundle atomically, idempotently, and without replacing divergent ids. */
|
|
816
|
+
export async function putAssuranceDeclarationBundle(pmRoot, bundle, options = {}) {
|
|
817
|
+
const candidate = createEmptyAssuranceDocument();
|
|
818
|
+
candidate.measurements = structuredClone(bundle.measurements);
|
|
819
|
+
candidate.assertions = structuredClone(bundle.assertions);
|
|
820
|
+
candidate.gates = structuredClone(bundle.gates);
|
|
821
|
+
validateAssuranceDocument(candidate);
|
|
822
|
+
const settings = await readSettings(pmRoot);
|
|
823
|
+
const author = resolveAuthor(options.author, settings.author_default);
|
|
824
|
+
const mutation = await mutateWorkspaceJsonWithHistory({
|
|
825
|
+
pmRoot,
|
|
826
|
+
filePath: assurancePath(pmRoot),
|
|
827
|
+
op: "assurance:bundle:apply",
|
|
828
|
+
author,
|
|
829
|
+
message: options.message,
|
|
830
|
+
lockTtlSeconds: LOCK_TTL_SECONDS,
|
|
831
|
+
lockWaitMs: LOCK_WAIT_MS,
|
|
832
|
+
mutate: (beforeRaw) => {
|
|
833
|
+
const document = beforeRaw === null
|
|
834
|
+
? createEmptyAssuranceDocument()
|
|
835
|
+
: parseAssuranceDocument(beforeRaw);
|
|
836
|
+
const createdIds = [];
|
|
837
|
+
const unchangedIds = [];
|
|
838
|
+
const apply = (target, additions, kind) => {
|
|
839
|
+
for (const definition of additions) {
|
|
840
|
+
const existing = target.find((entry) => entry.id === definition.id);
|
|
841
|
+
if (!existing) {
|
|
842
|
+
target.push(structuredClone(definition));
|
|
843
|
+
createdIds.push(`${kind}:${definition.id}`);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (stableStringify(existing) !== stableStringify(definition)) {
|
|
847
|
+
throw new AssuranceMutationRefusalError(`assurance bundle refuses to replace divergent ${kind} ${definition.id}`);
|
|
848
|
+
}
|
|
849
|
+
unchangedIds.push(`${kind}:${definition.id}`);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
apply(document.measurements, candidate.measurements, "measurement");
|
|
853
|
+
apply(document.assertions, candidate.assertions, "assertion");
|
|
854
|
+
apply(document.gates, candidate.gates, "gate");
|
|
855
|
+
validateAssuranceDocument(document);
|
|
856
|
+
return {
|
|
857
|
+
raw: `${JSON.stringify(document, null, 2)}\n`,
|
|
858
|
+
result: {
|
|
859
|
+
changed: createdIds.length > 0,
|
|
860
|
+
created_ids: createdIds,
|
|
861
|
+
unchanged_ids: unchangedIds,
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
return { ...mutation.result, changed: mutation.changed };
|
|
867
|
+
}
|
|
621
868
|
/** Remove an unreferenced declaration through audited singleton persistence. */
|
|
622
869
|
export async function removeAssuranceDeclaration(pmRoot, kind, id, options = {}) {
|
|
623
|
-
const
|
|
870
|
+
const settings = await readSettings(pmRoot);
|
|
871
|
+
const author = resolveAuthor(options.author, settings.author_default);
|
|
624
872
|
const mutation = await mutateWorkspaceJsonWithHistory({
|
|
625
873
|
pmRoot,
|
|
626
874
|
filePath: assurancePath(pmRoot),
|
|
@@ -630,20 +878,24 @@ export async function removeAssuranceDeclaration(pmRoot, kind, id, options = {})
|
|
|
630
878
|
lockTtlSeconds: LOCK_TTL_SECONDS,
|
|
631
879
|
lockWaitMs: LOCK_WAIT_MS,
|
|
632
880
|
mutate: (beforeRaw) => {
|
|
633
|
-
const document = beforeRaw === null
|
|
881
|
+
const document = beforeRaw === null
|
|
882
|
+
? createEmptyAssuranceDocument()
|
|
883
|
+
: parseAssuranceDocument(beforeRaw);
|
|
634
884
|
if (kind === "measurement") {
|
|
635
885
|
const consumers = collectMeasurementReferences(document).get(id);
|
|
636
886
|
if (consumers && consumers.length > 0) {
|
|
637
|
-
throw new
|
|
887
|
+
throw new AssuranceMutationRefusalError(`assurance measurement ${id} is referenced by ${consumers.join(", ")}`);
|
|
638
888
|
}
|
|
639
889
|
}
|
|
640
|
-
if (kind === "assertion" &&
|
|
641
|
-
|
|
890
|
+
if (kind === "assertion" &&
|
|
891
|
+
document.gates.some((entry) => entry.assertion_ids.includes(id))) {
|
|
892
|
+
throw new AssuranceMutationRefusalError(`assurance assertion ${id} is referenced by gate`);
|
|
642
893
|
}
|
|
643
894
|
const collection = collectionFor(document, kind);
|
|
644
895
|
const index = collection.findIndex((entry) => entry.id === id);
|
|
645
|
-
if (index < 0)
|
|
646
|
-
throw new
|
|
896
|
+
if (index < 0) {
|
|
897
|
+
throw new AssuranceMutationRefusalError(`assurance ${kind} ${id} not found`);
|
|
898
|
+
}
|
|
647
899
|
collection.splice(index, 1);
|
|
648
900
|
validateAssuranceDocument(document);
|
|
649
901
|
return {
|
|
@@ -658,12 +910,14 @@ export async function removeAssuranceDeclaration(pmRoot, kind, id, options = {})
|
|
|
658
910
|
export async function recordAssuranceVerdict(pmRoot, verdict, options = {}) {
|
|
659
911
|
if (verdict.dry_run)
|
|
660
912
|
throw new TypeError("dry-run assurance verdicts are not persisted");
|
|
913
|
+
const settings = await readSettings(pmRoot);
|
|
661
914
|
await appendWorkspaceAuditEvent({
|
|
662
915
|
pmRoot,
|
|
663
916
|
op: "assurance:gate:verdict",
|
|
664
|
-
author: resolveAuthor(options.author,
|
|
917
|
+
author: resolveAuthor(options.author, settings.author_default),
|
|
665
918
|
context: { assurance_verdict: verdict },
|
|
666
|
-
message: options.message ??
|
|
919
|
+
message: options.message ??
|
|
920
|
+
`Assurance gate ${verdict.gate_id} returned ${verdict.verdict}`,
|
|
667
921
|
lockTtlSeconds: LOCK_TTL_SECONDS,
|
|
668
922
|
lockWaitMs: LOCK_WAIT_MS,
|
|
669
923
|
});
|
|
@@ -686,4 +940,4 @@ export async function listAssuranceVerdicts(pmRoot, options = {}) {
|
|
|
686
940
|
.slice(0, limit);
|
|
687
941
|
}
|
|
688
942
|
//# sourceMappingURL=assurance.js.map
|
|
689
|
-
//# debugId=
|
|
943
|
+
//# debugId=e491ddd9-60e7-51fd-b0c7-4f6e86ee56f9
|