sentinelayer-cli 0.46.5 → 0.48.0
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/README.md +47 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +32 -1
- package/src/auth/session-store.js +37 -0
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +516 -30
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +25 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +110 -15
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW02 — Versioned contracts barrel.
|
|
3
|
+
*
|
|
4
|
+
* This is the single validated contract set for all parallel agents (ticket
|
|
5
|
+
* 73ead329). It freezes the graph, capability, evidence, and money contracts
|
|
6
|
+
* so every consumer validates against the same schemas.
|
|
7
|
+
*
|
|
8
|
+
* Module name: agent-web-contracts (per ticket proposal).
|
|
9
|
+
* Actual location: src/engram/contracts/ (within the detachable ENGRAM tree).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Money contract (§ step-9, step-10)
|
|
13
|
+
export {
|
|
14
|
+
KNOWN_CURRENCIES,
|
|
15
|
+
moneySchema,
|
|
16
|
+
migrateLegacyMoney,
|
|
17
|
+
minorUnitsSchema,
|
|
18
|
+
decimalStringSchema,
|
|
19
|
+
toMinorUnits,
|
|
20
|
+
formatFromMinorUnits,
|
|
21
|
+
currencyExponent,
|
|
22
|
+
} from "./money.js";
|
|
23
|
+
|
|
24
|
+
// Graph contract (R02 graph, R09 truth)
|
|
25
|
+
export {
|
|
26
|
+
MONEY_FIELD_NAMES,
|
|
27
|
+
PROVENANCE_CLASSES,
|
|
28
|
+
SCHEMA_VALIDITY,
|
|
29
|
+
MODEL_CONFIDENCE,
|
|
30
|
+
provenanceClassSchema,
|
|
31
|
+
schemaValiditySchema,
|
|
32
|
+
modelConfidenceSchema,
|
|
33
|
+
quantitySchema,
|
|
34
|
+
entitySchema,
|
|
35
|
+
linkSchema,
|
|
36
|
+
freshnessSchema,
|
|
37
|
+
assertionSchema,
|
|
38
|
+
graphSchema,
|
|
39
|
+
classifyProvenance,
|
|
40
|
+
validateGraphIntegrity,
|
|
41
|
+
} from "./graph.js";
|
|
42
|
+
|
|
43
|
+
// Capability contract (R04 skills, R14 parallelism)
|
|
44
|
+
export {
|
|
45
|
+
capabilityVersionSchema,
|
|
46
|
+
tenantScopeSchema,
|
|
47
|
+
tenantScopeDescriptorSchema,
|
|
48
|
+
sourceDescriptorSchema,
|
|
49
|
+
capabilityEntrySchema,
|
|
50
|
+
negotiationResultSchema,
|
|
51
|
+
paginationSchema,
|
|
52
|
+
migrationPolicySchema,
|
|
53
|
+
CapabilityError,
|
|
54
|
+
unknownCapability,
|
|
55
|
+
unsupportedCapability,
|
|
56
|
+
staleCapability,
|
|
57
|
+
negotiateCapability,
|
|
58
|
+
} from "./capability.js";
|
|
59
|
+
|
|
60
|
+
// Evidence contract (R09 truth)
|
|
61
|
+
export {
|
|
62
|
+
evidenceAddressSchema,
|
|
63
|
+
evidenceRecordSchema,
|
|
64
|
+
evidencePageSchema,
|
|
65
|
+
evidenceMigrationPolicySchema,
|
|
66
|
+
evidenceFreshnessSchema,
|
|
67
|
+
EvidenceError,
|
|
68
|
+
evidenceNotFound,
|
|
69
|
+
evidenceStale,
|
|
70
|
+
evidenceUnsupportedScheme,
|
|
71
|
+
validateNotScreenshotInferred,
|
|
72
|
+
} from "./evidence.js";
|
|
73
|
+
|
|
74
|
+
// SourceSnapshot contract
|
|
75
|
+
export {
|
|
76
|
+
sourceSnapshotSchema,
|
|
77
|
+
VALID_SOURCE_SNAPSHOT,
|
|
78
|
+
MINIMAL_SOURCE_SNAPSHOT,
|
|
79
|
+
} from "./source-snapshot.js";
|
|
80
|
+
|
|
81
|
+
// MediaAsset contract
|
|
82
|
+
export {
|
|
83
|
+
MEDIA_ASSET_TYPES,
|
|
84
|
+
mediaAssetTypeSchema,
|
|
85
|
+
mediaAssetSchema,
|
|
86
|
+
VALID_MEDIA_ASSET,
|
|
87
|
+
} from "./media-asset.js";
|
|
88
|
+
|
|
89
|
+
// TemporalSegment contract
|
|
90
|
+
export {
|
|
91
|
+
TEMPORAL_SEGMENT_KINDS,
|
|
92
|
+
temporalSegmentKindSchema,
|
|
93
|
+
temporalSegmentSchema,
|
|
94
|
+
VALID_TEMPORAL_SEGMENT,
|
|
95
|
+
} from "./temporal-segment.js";
|
|
96
|
+
|
|
97
|
+
// TaskContextPack contract
|
|
98
|
+
export {
|
|
99
|
+
delegationReferenceSchema,
|
|
100
|
+
taskContextPackSchema,
|
|
101
|
+
VALID_TASK_CONTEXT_PACK,
|
|
102
|
+
} from "./task-context-pack.js";
|
|
103
|
+
|
|
104
|
+
// Contract versions
|
|
105
|
+
export {
|
|
106
|
+
CONTRACT_VERSIONS,
|
|
107
|
+
GRAPH_SCHEMA_VERSION,
|
|
108
|
+
EVIDENCE_SCHEMA_VERSION,
|
|
109
|
+
COMPAT_FIXTURES,
|
|
110
|
+
} from "./versions.js";
|
|
111
|
+
|
|
112
|
+
// ActionReceipt contract
|
|
113
|
+
export {
|
|
114
|
+
ACTION_RECEIPT_STATUSES,
|
|
115
|
+
actionReceiptStatusSchema,
|
|
116
|
+
actionReceiptSchema,
|
|
117
|
+
VALID_ACTION_RECEIPT,
|
|
118
|
+
} from "./action-receipt.js";
|
|
119
|
+
|
|
120
|
+
// Capability Index contract (EW08)
|
|
121
|
+
export {
|
|
122
|
+
skillSchema,
|
|
123
|
+
capabilityIndexSchema,
|
|
124
|
+
CapabilityIndexError,
|
|
125
|
+
digestMismatch,
|
|
126
|
+
indexExpired,
|
|
127
|
+
originMismatch,
|
|
128
|
+
capabilityRevoked,
|
|
129
|
+
} from "./capability-index.js";
|
|
130
|
+
|
|
131
|
+
// Live-state contract (EW14)
|
|
132
|
+
export {
|
|
133
|
+
FRESHNESS_CLASSES,
|
|
134
|
+
classifyFreshness,
|
|
135
|
+
isValidFreshnessClass,
|
|
136
|
+
createFreshnessMarker,
|
|
137
|
+
isFreshnessStale,
|
|
138
|
+
FreshnessClassError,
|
|
139
|
+
SOURCE_STATUSES,
|
|
140
|
+
createSourceVersion,
|
|
141
|
+
resolveAuthoritativeSource,
|
|
142
|
+
SourceVersionError,
|
|
143
|
+
mustRevalidate,
|
|
144
|
+
RevalidationRequiredError,
|
|
145
|
+
disabledByOutage,
|
|
146
|
+
SourceOutageError,
|
|
147
|
+
createCommerceContext,
|
|
148
|
+
CommerceContextError,
|
|
149
|
+
} from "../live-state.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW02 — MediaAsset contract.
|
|
3
|
+
*
|
|
4
|
+
* A MediaAsset represents any media resource referenced by the ENGRAM graph:
|
|
5
|
+
* images, audio, video, documents, or other binary/textual assets. Every
|
|
6
|
+
* asset must declare its address scheme, content hash for integrity, and
|
|
7
|
+
* a source snapshot reference.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { sourceSnapshotSchema } from "./source-snapshot.js";
|
|
12
|
+
import { sha256ContentHashSchema } from "./content-hash.js";
|
|
13
|
+
|
|
14
|
+
export const MEDIA_ASSET_TYPES = Object.freeze([
|
|
15
|
+
"image",
|
|
16
|
+
"audio",
|
|
17
|
+
"video",
|
|
18
|
+
"document",
|
|
19
|
+
"text",
|
|
20
|
+
"binary",
|
|
21
|
+
"other",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const mediaAssetTypeSchema = z.enum([
|
|
25
|
+
"image",
|
|
26
|
+
"audio",
|
|
27
|
+
"video",
|
|
28
|
+
"document",
|
|
29
|
+
"text",
|
|
30
|
+
"binary",
|
|
31
|
+
"other",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A media asset in the ENGRAM graph.
|
|
36
|
+
*
|
|
37
|
+
* Every asset carries a content hash for integrity verification and a
|
|
38
|
+
* source snapshot for provenance. Assets without a source snapshot cannot
|
|
39
|
+
* be treated as authoritative evidence.
|
|
40
|
+
*/
|
|
41
|
+
export const mediaAssetSchema = z.object({
|
|
42
|
+
assetId: z.string().min(1),
|
|
43
|
+
type: mediaAssetTypeSchema,
|
|
44
|
+
uri: z.string().min(1),
|
|
45
|
+
contentHash: sha256ContentHashSchema,
|
|
46
|
+
mimeType: z.string().optional(),
|
|
47
|
+
sizeBytes: z.number().int().positive().optional(),
|
|
48
|
+
sourceSnapshot: sourceSnapshotSchema,
|
|
49
|
+
metadata: z.record(z.string(), z.json()).optional(),
|
|
50
|
+
tenantScope: z.string().min(1),
|
|
51
|
+
}).strict().refine(
|
|
52
|
+
(data) => {
|
|
53
|
+
// Tenant scope must be consistent: top-level tenantScope must match
|
|
54
|
+
// the sourceSnapshot's tenantScope, or an explicit authorization boundary
|
|
55
|
+
// must be declared. Without authorization, cross-tenant is refused.
|
|
56
|
+
if (data.tenantScope !== data.sourceSnapshot.tenantScope) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
message: "tenantScope must match sourceSnapshot.tenantScope; cross-tenant requires explicit authorization",
|
|
63
|
+
path: ["tenantScope"],
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Valid fixture: a complete, valid media asset.
|
|
69
|
+
*/
|
|
70
|
+
export const VALID_MEDIA_ASSET = Object.freeze({
|
|
71
|
+
assetId: "ma-img-001",
|
|
72
|
+
type: "image",
|
|
73
|
+
uri: "https://evidence.example.com/assets/img-001.png",
|
|
74
|
+
contentHash: "sha256:a6f2f770889daacf470cedb54e76c426817431a5e18aff6b270f182f22e0d43d",
|
|
75
|
+
mimeType: "image/png",
|
|
76
|
+
sizeBytes: 102400,
|
|
77
|
+
sourceSnapshot: {
|
|
78
|
+
sourceId: "src-session-abc123",
|
|
79
|
+
sourceType: "session",
|
|
80
|
+
version: "1.2.0",
|
|
81
|
+
capturedAt: "2026-10-23T12:00:00Z",
|
|
82
|
+
tenantScope: "tenant/acme",
|
|
83
|
+
},
|
|
84
|
+
tenantScope: "tenant/acme",
|
|
85
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW02 — Money contract: integer minor units + currency + exponent,
|
|
3
|
+
* or validated decimal strings. Never binary floats.
|
|
4
|
+
*
|
|
5
|
+
* Design constraint (ticket 73ead329): money is integer minor units plus
|
|
6
|
+
* currency and correct exponent, or validated decimal strings; never binary
|
|
7
|
+
* floats for totals. This module is the single codified source of truth for
|
|
8
|
+
* every path that stores, reads, or validates monetary values in ENGRAM.
|
|
9
|
+
*
|
|
10
|
+
* Luna-judge F5: use exact BigInt conversion, never floating-point rounding.
|
|
11
|
+
* Luna-judge F8: minorUnitsSchema must check both safe integer bounds.
|
|
12
|
+
* Luna-judge P2: validate against a complete currency registry; reject unknown codes.
|
|
13
|
+
* Luna-judge P2: strict schema — reject extra fields.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ISO 4217 currency codes (supported subset validated at runtime).
|
|
20
|
+
*/
|
|
21
|
+
const CURRENCY_RE = /^[A-Z]{3}$/;
|
|
22
|
+
const CURRENCY_MINOR_UNITS = new Map([
|
|
23
|
+
["USD", 2], ["EUR", 2], ["GBP", 2], ["JPY", 0], ["KRW", 0],
|
|
24
|
+
["CNY", 2], ["INR", 2], ["CAD", 2], ["AUD", 2], ["CHF", 2],
|
|
25
|
+
["BRL", 2], ["MXN", 2], ["SEK", 2], ["NOK", 2], ["DKK", 2],
|
|
26
|
+
["PLN", 2], ["RUB", 2], ["TRY", 2], ["ZAR", 2], ["NGN", 2],
|
|
27
|
+
["BHD", 3], ["KWD", 3], ["OMR", 3], ["JOD", 3], ["TND", 3],
|
|
28
|
+
["CLF", 4], ["UYW", 4],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Complete list of known currency codes — used to reject unknown codes. */
|
|
32
|
+
export const KNOWN_CURRENCIES = new Set(CURRENCY_MINOR_UNITS.keys());
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reject binary floating-point in monetary positions.
|
|
36
|
+
* NaN, Infinity, -Infinity, non-integer, and unsafe integers are all refused.
|
|
37
|
+
*/
|
|
38
|
+
function rejectFloat(value, ctx) {
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
if (!Number.isFinite(value)) {
|
|
41
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "monetary value must be finite; got non-finite number" });
|
|
42
|
+
return z.NEVER;
|
|
43
|
+
}
|
|
44
|
+
if (!Number.isInteger(value)) {
|
|
45
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "monetary value must be an integer (minor units); binary floats are not allowed for totals" });
|
|
46
|
+
return z.NEVER;
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isSafeInteger(value)) {
|
|
49
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "monetary value exceeds safe integer range; use decimal string instead" });
|
|
50
|
+
return z.NEVER;
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Validated decimal string: no scientific notation. */
|
|
58
|
+
const DECIMAL_STRING_RE = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/;
|
|
59
|
+
|
|
60
|
+
/** Minor-units integer: safe, finite, both bounds checked (F8). */
|
|
61
|
+
export const minorUnitsSchema = z.number().int().finite().refine(
|
|
62
|
+
(v) => Number.isSafeInteger(v),
|
|
63
|
+
{ message: "minor units outside safe representable range" },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/** Decimal string that parses cleanly and never encodes scientific notation. */
|
|
67
|
+
export const decimalStringSchema = z.string().regex(
|
|
68
|
+
DECIMAL_STRING_RE,
|
|
69
|
+
{ message: "money decimal string must match /^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$/; no scientific notation" },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The single canonical money schema — strict: rejects extra fields.
|
|
74
|
+
*
|
|
75
|
+
* A well-formed monetary value in ENGRAM is exactly one of:
|
|
76
|
+
* { amount: <integer minor units>, amountUnit: "minor_units", currency: "USD", exponent: 2 }
|
|
77
|
+
* { amount: "-?[0-9]+(\\.[0-9]+)?", amountUnit: "major_decimal", currency: "USD", exponent: 2 }
|
|
78
|
+
*
|
|
79
|
+
* Binary floats (1.99), NaN, Infinity, and unsafe integers are REFUSED.
|
|
80
|
+
* Unknown currency codes are REFUSED (P2: pinned registry only).
|
|
81
|
+
* Extra fields are REFUSED (P2: strict).
|
|
82
|
+
*/
|
|
83
|
+
export const moneySchema = z.object({
|
|
84
|
+
amount: z.union([
|
|
85
|
+
z.number().superRefine(rejectFloat),
|
|
86
|
+
decimalStringSchema,
|
|
87
|
+
]),
|
|
88
|
+
// Do not infer units from JSON number/string representation. A lost quote
|
|
89
|
+
// must fail validation rather than silently changing a total by 10^exponent.
|
|
90
|
+
amountUnit: z.enum(["minor_units", "major_decimal"]),
|
|
91
|
+
currency: z.string().regex(CURRENCY_RE, { message: "currency must be a 3-letter ISO 4217 code" }),
|
|
92
|
+
exponent: z.number().int().min(0).max(6).optional(),
|
|
93
|
+
}).strict().refine(
|
|
94
|
+
(data) => (data.amountUnit === "minor_units") === (typeof data.amount === "number"),
|
|
95
|
+
{ message: "amountUnit must match numeric minor units or string major decimal", path: ["amountUnit"] },
|
|
96
|
+
).refine(
|
|
97
|
+
(data) => {
|
|
98
|
+
if (!KNOWN_CURRENCIES.has(data.currency)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
{ message: "currency code is not in the supported ISO 4217 registry; register it before use", path: ["currency"] },
|
|
104
|
+
).refine(
|
|
105
|
+
(data) => {
|
|
106
|
+
const known = CURRENCY_MINOR_UNITS.get(data.currency);
|
|
107
|
+
if (data.exponent != null && known != null && data.exponent !== known) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
{ message: "exponent must match the currency's standard minor units when both are known", path: ["exponent"] },
|
|
113
|
+
).refine(
|
|
114
|
+
(data) => {
|
|
115
|
+
// Require exponent for decimal strings or use currency default
|
|
116
|
+
const exponent = data.exponent ?? CURRENCY_MINOR_UNITS.get(data.currency);
|
|
117
|
+
if (exponent == null) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (typeof data.amount === "string") {
|
|
121
|
+
const dot = data.amount.indexOf(".");
|
|
122
|
+
if (dot >= 0) {
|
|
123
|
+
const fracLen = data.amount.length - dot - 1;
|
|
124
|
+
if (fracLen > exponent) return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
{ message: "currency requires a known exponent; provide exponent or use a known currency code", path: ["exponent"] },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Migrate a pre-freeze `{amount, currency, exponent}` value only when the
|
|
134
|
+
* importing source explicitly declares its old unit convention. Never guess
|
|
135
|
+
* a unit from a number/string, since that can multiply a total by 10^exponent.
|
|
136
|
+
* Returns null for malformed or already-tagged input.
|
|
137
|
+
*/
|
|
138
|
+
export function migrateLegacyMoney(legacy, amountUnit) {
|
|
139
|
+
if (!legacy || typeof legacy !== "object" || Array.isArray(legacy) || "amountUnit" in legacy) return null;
|
|
140
|
+
const result = moneySchema.safeParse({ ...legacy, amountUnit });
|
|
141
|
+
return result.success ? result.data : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Exact decimal multiplication using BigInt for lossless conversion.
|
|
146
|
+
* Parses a decimal string and multiplies by 10^exp using integer arithmetic.
|
|
147
|
+
* @param {string} decimal — e.g. "19.99" or "90071992547409.90"
|
|
148
|
+
* @param {number} exponent — minor-units exponent
|
|
149
|
+
* @returns {bigint|null}
|
|
150
|
+
*/
|
|
151
|
+
function decimalToMinorUnitsExact(decimal, exponent) {
|
|
152
|
+
const negative = decimal.startsWith("-");
|
|
153
|
+
const abs = negative ? decimal.slice(1) : decimal;
|
|
154
|
+
|
|
155
|
+
const dot = abs.indexOf(".");
|
|
156
|
+
let integerPart, fracPart;
|
|
157
|
+
if (dot >= 0) {
|
|
158
|
+
integerPart = abs.slice(0, dot);
|
|
159
|
+
fracPart = abs.slice(dot + 1).padEnd(exponent, "0").slice(0, exponent);
|
|
160
|
+
// Reject extra fractional digits beyond the exponent
|
|
161
|
+
const origFracLen = abs.length - dot - 1;
|
|
162
|
+
if (origFracLen > exponent) return null;
|
|
163
|
+
} else {
|
|
164
|
+
integerPart = abs;
|
|
165
|
+
fracPart = "0".repeat(exponent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pad fraction to exactly exponent digits
|
|
169
|
+
if (fracPart.length < exponent) {
|
|
170
|
+
fracPart = fracPart.padEnd(exponent, "0");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
const int = BigInt(integerPart);
|
|
176
|
+
const frac = BigInt(fracPart);
|
|
177
|
+
const multiplier = 10n ** BigInt(exponent);
|
|
178
|
+
result = int * multiplier + frac;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return negative ? -result : result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Normalize a money value to its canonical minor-units integer representation.
|
|
188
|
+
* Uses exact BigInt conversion for decimal strings (F5).
|
|
189
|
+
* Validates the input: rejects extra fractional digits that would truncate.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} money — an explicitly unit-tagged money object
|
|
192
|
+
* @returns {number|null}
|
|
193
|
+
*/
|
|
194
|
+
export function toMinorUnits(money) {
|
|
195
|
+
const parsed = moneySchema.safeParse(money);
|
|
196
|
+
if (!parsed.success) return null;
|
|
197
|
+
const value = parsed.data;
|
|
198
|
+
const exponent = value.exponent ?? CURRENCY_MINOR_UNITS.get(value.currency);
|
|
199
|
+
if (exponent == null) return null;
|
|
200
|
+
|
|
201
|
+
if (value.amountUnit === "minor_units") return value.amount;
|
|
202
|
+
|
|
203
|
+
// Validate: reject if fractional digits exceed exponent (would truncate)
|
|
204
|
+
const dot = value.amount.indexOf(".");
|
|
205
|
+
if (dot >= 0) {
|
|
206
|
+
const fracLen = value.amount.length - dot - 1;
|
|
207
|
+
if (fracLen > exponent) return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Exact BigInt conversion for decimal strings
|
|
211
|
+
const exact = decimalToMinorUnitsExact(value.amount, exponent);
|
|
212
|
+
if (exact == null) return null;
|
|
213
|
+
|
|
214
|
+
// Check safe integer range before converting back to Number
|
|
215
|
+
if (exact > 9007199254740991n || exact < -9007199254740991n) return null;
|
|
216
|
+
|
|
217
|
+
return Number(exact);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format minor units back to a decimal string using exact BigInt arithmetic (F5).
|
|
222
|
+
* @param {number} minorUnits
|
|
223
|
+
* @param {number} exponent
|
|
224
|
+
* @returns {string}
|
|
225
|
+
* @throws {RangeError} when minorUnits is not a safe integer or exponent is outside 0..6
|
|
226
|
+
*/
|
|
227
|
+
export function formatFromMinorUnits(minorUnits, exponent = 2) {
|
|
228
|
+
if (!Number.isSafeInteger(minorUnits)) {
|
|
229
|
+
throw new RangeError("minorUnits must be a finite safe integer");
|
|
230
|
+
}
|
|
231
|
+
if (!Number.isInteger(exponent) || exponent < 0 || exponent > 6) {
|
|
232
|
+
throw new RangeError("exponent must be an integer from 0 to 6");
|
|
233
|
+
}
|
|
234
|
+
if (exponent === 0) return String(minorUnits);
|
|
235
|
+
|
|
236
|
+
const negative = minorUnits < 0;
|
|
237
|
+
const abs = negative ? -minorUnits : minorUnits;
|
|
238
|
+
const divisor = 10n ** BigInt(exponent);
|
|
239
|
+
const bigAbs = BigInt(abs);
|
|
240
|
+
const whole = bigAbs / divisor;
|
|
241
|
+
const frac = String(bigAbs % divisor).padStart(exponent, "0");
|
|
242
|
+
|
|
243
|
+
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Known minor-unit exponent for a currency code (null if unknown). */
|
|
247
|
+
export function currencyExponent(currency) {
|
|
248
|
+
return CURRENCY_MINOR_UNITS.get(currency) ?? null;
|
|
249
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW02 — SourceSnapshot contract.
|
|
3
|
+
*
|
|
4
|
+
* A SourceSnapshot captures the provenance of every fact: what source produced
|
|
5
|
+
* it, at what version, when, and under which tenant scope. No assertion is
|
|
6
|
+
* authoritative without a reachable, versioned source snapshot.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { nonblankIdentitySchema } from "./identity.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A point-in-time snapshot of a data source.
|
|
14
|
+
*
|
|
15
|
+
* Every fact in the ENGRAM graph MUST be traceable to a source snapshot that
|
|
16
|
+
* declares its version, capture time, and tenant scope. Absence of any of
|
|
17
|
+
* these fields means the fact cannot be classified as authoritative.
|
|
18
|
+
*/
|
|
19
|
+
export const sourceSnapshotSchema = z.object({
|
|
20
|
+
sourceId: nonblankIdentitySchema,
|
|
21
|
+
sourceType: z.enum([
|
|
22
|
+
"session",
|
|
23
|
+
"api",
|
|
24
|
+
"ingest",
|
|
25
|
+
"manual",
|
|
26
|
+
"derived",
|
|
27
|
+
"import",
|
|
28
|
+
]),
|
|
29
|
+
version: nonblankIdentitySchema,
|
|
30
|
+
capturedAt: z.string().datetime(),
|
|
31
|
+
tenantScope: nonblankIdentitySchema,
|
|
32
|
+
contentHash: z.string().min(1).optional(),
|
|
33
|
+
metadata: z.record(z.string(), z.json()).optional(),
|
|
34
|
+
}).strict();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Valid fixture: a complete, valid source snapshot.
|
|
38
|
+
*/
|
|
39
|
+
export const VALID_SOURCE_SNAPSHOT = Object.freeze({
|
|
40
|
+
sourceId: "src-session-abc123",
|
|
41
|
+
sourceType: "session",
|
|
42
|
+
version: "1.2.0",
|
|
43
|
+
capturedAt: "2026-10-23T12:00:00Z",
|
|
44
|
+
tenantScope: "tenant/acme",
|
|
45
|
+
contentHash: "sha256:abc123def456",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A minimal but valid source snapshot (no optional fields).
|
|
50
|
+
*/
|
|
51
|
+
export const MINIMAL_SOURCE_SNAPSHOT = Object.freeze({
|
|
52
|
+
sourceId: "src-min-001",
|
|
53
|
+
sourceType: "manual",
|
|
54
|
+
version: "0.1.0",
|
|
55
|
+
capturedAt: "2026-10-23T00:00:00Z",
|
|
56
|
+
tenantScope: "tenant/default",
|
|
57
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW02 — TaskContextPack contract.
|
|
3
|
+
*
|
|
4
|
+
* A TaskContextPack bundles the minimal context needed for a parallel agent
|
|
5
|
+
* to operate: capabilities, evidence references, and delegation instructions.
|
|
6
|
+
* Every pack carries its own version, tenant scope, and source snapshot.
|
|
7
|
+
*
|
|
8
|
+
* Cross-field tenant validation: source snapshots must share the pack's
|
|
9
|
+
* tenant scope. A pack cannot grant itself a cross-tenant exception.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { sourceSnapshotSchema } from "./source-snapshot.js";
|
|
14
|
+
import { capabilityVersionSchema } from "./capability.js";
|
|
15
|
+
import { CONTRACT_VERSIONS } from "./versions.js";
|
|
16
|
+
|
|
17
|
+
// Bounds are bare SemVer versions of the named capability, inclusive.
|
|
18
|
+
const VERSION_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
19
|
+
const boundVersionSchema = z.string().regex(VERSION_RE);
|
|
20
|
+
|
|
21
|
+
function compareVersions(left, right) {
|
|
22
|
+
const a = left.split(".").map(BigInt);
|
|
23
|
+
const b = right.split(".").map(BigInt);
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A delegation reference: what agent or capability is delegated, with
|
|
32
|
+
* constraints and version requirements.
|
|
33
|
+
*/
|
|
34
|
+
export const delegationReferenceSchema = z.object({
|
|
35
|
+
delegateId: z.string().min(1),
|
|
36
|
+
capability: capabilityVersionSchema,
|
|
37
|
+
constraints: z.record(z.string(), z.json()).optional(),
|
|
38
|
+
minVersion: boundVersionSchema.optional(),
|
|
39
|
+
maxVersion: boundVersionSchema.optional(),
|
|
40
|
+
}).strict().superRefine((delegation, ctx) => {
|
|
41
|
+
const capabilityVersion = delegation.capability.split("@").at(-1);
|
|
42
|
+
if (!VERSION_RE.test(capabilityVersion)) return;
|
|
43
|
+
if (delegation.minVersion && VERSION_RE.test(delegation.minVersion) &&
|
|
44
|
+
compareVersions(capabilityVersion, delegation.minVersion) < 0) {
|
|
45
|
+
ctx.addIssue({ code: "custom", path: ["minVersion"], message: "delegated capability is below minVersion" });
|
|
46
|
+
}
|
|
47
|
+
if (delegation.maxVersion && VERSION_RE.test(delegation.maxVersion) &&
|
|
48
|
+
compareVersions(capabilityVersion, delegation.maxVersion) > 0) {
|
|
49
|
+
ctx.addIssue({ code: "custom", path: ["maxVersion"], message: "delegated capability is above maxVersion" });
|
|
50
|
+
}
|
|
51
|
+
if (delegation.minVersion && delegation.maxVersion &&
|
|
52
|
+
VERSION_RE.test(delegation.minVersion) && VERSION_RE.test(delegation.maxVersion) &&
|
|
53
|
+
compareVersions(delegation.minVersion, delegation.maxVersion) > 0) {
|
|
54
|
+
ctx.addIssue({ code: "custom", path: ["maxVersion"], message: "maxVersion must not precede minVersion" });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A task context pack: the minimal validated context for a parallel agent.
|
|
60
|
+
*
|
|
61
|
+
* Packs carry capabilities, evidence references, and delegation instructions
|
|
62
|
+
* so every agent gets a consistent, validated starting context.
|
|
63
|
+
*
|
|
64
|
+
* Tenant scope reconciliation: nested objects must share the pack's tenant
|
|
65
|
+
* scope. Cross-tenant delegation needs a separate, trusted authorization
|
|
66
|
+
* boundary rather than a self-declared field in an incoming pack.
|
|
67
|
+
*/
|
|
68
|
+
export const taskContextPackSchema = z.object({
|
|
69
|
+
packId: z.string().min(1),
|
|
70
|
+
version: z.literal(CONTRACT_VERSIONS.taskContextPack),
|
|
71
|
+
tenantScope: z.string().min(1),
|
|
72
|
+
sourceSnapshot: sourceSnapshotSchema,
|
|
73
|
+
capabilities: z.array(capabilityVersionSchema).optional(),
|
|
74
|
+
evidenceRefs: z.array(z.string().min(1)).optional(),
|
|
75
|
+
delegations: z.array(delegationReferenceSchema).optional(),
|
|
76
|
+
createdAt: z.string().datetime(),
|
|
77
|
+
expiresAt: z.string().datetime().optional(),
|
|
78
|
+
metadata: z.record(z.string(), z.json()).optional(),
|
|
79
|
+
}).strict().superRefine((data, ctx) => {
|
|
80
|
+
if (data.sourceSnapshot.tenantScope !== data.tenantScope) {
|
|
81
|
+
ctx.addIssue({ code: "custom", message: "sourceSnapshot tenantScope must match pack tenantScope", path: ["sourceSnapshot", "tenantScope"] });
|
|
82
|
+
}
|
|
83
|
+
if (Date.parse(data.sourceSnapshot.capturedAt) > Date.parse(data.createdAt)) {
|
|
84
|
+
ctx.addIssue({ code: "custom", message: "sourceSnapshot cannot be captured after pack creation", path: ["sourceSnapshot", "capturedAt"] });
|
|
85
|
+
}
|
|
86
|
+
if (data.expiresAt && Date.parse(data.expiresAt) <= Date.parse(data.createdAt)) {
|
|
87
|
+
ctx.addIssue({ code: "custom", message: "expiresAt must be after createdAt", path: ["expiresAt"] });
|
|
88
|
+
}
|
|
89
|
+
const declared = new Set(data.capabilities ?? []);
|
|
90
|
+
for (const [index, delegation] of (data.delegations ?? []).entries()) {
|
|
91
|
+
if (!declared.has(delegation.capability)) {
|
|
92
|
+
ctx.addIssue({ code: "custom", message: "delegation capability must be declared by this pack", path: ["delegations", index, "capability"] });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Valid fixture: a complete task context pack.
|
|
99
|
+
*/
|
|
100
|
+
export const VALID_TASK_CONTEXT_PACK = Object.freeze({
|
|
101
|
+
packId: "tcp-ew02-001",
|
|
102
|
+
version: "1.0.0",
|
|
103
|
+
tenantScope: "tenant/acme",
|
|
104
|
+
sourceSnapshot: {
|
|
105
|
+
sourceId: "src-session-abc123",
|
|
106
|
+
sourceType: "session",
|
|
107
|
+
version: "1.2.0",
|
|
108
|
+
capturedAt: "2026-10-23T12:00:00Z",
|
|
109
|
+
tenantScope: "tenant/acme",
|
|
110
|
+
},
|
|
111
|
+
capabilities: ["graph-read@1.0.0"],
|
|
112
|
+
evidenceRefs: ["evi-abc123"],
|
|
113
|
+
createdAt: "2026-10-23T12:00:00Z",
|
|
114
|
+
});
|