@aiwg/cli 2026.9.20 → 2026.9.21
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/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
5
|
+
import { compileCacheKey, validateCompileIdentity } from './identity.js';
|
|
6
|
+
import { activeCompileCacheHold, buildCompileCacheEntry, compileCacheLifecycleRule, compileCacheReference, compileCacheScopeAllowed, compileCacheTombstoneRecord, CompileCacheRejectedError, isCompileCacheTombstone, mayRestoreCompileCacheEntry, revalidateCompileCacheEntry, validateCompileCacheHold, } from './records.js';
|
|
7
|
+
const isTombstone = (record) => record.schemaVersion === 'decision-compile-cache-tombstone/v1';
|
|
8
|
+
const DEFAULT_LOCK_POLL_MS = 25;
|
|
9
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* Durable, atomic filesystem cache. Files are named only by one-way key digests.
|
|
12
|
+
* Out-of-scope or unauthorized lifecycle calls return the same result as an
|
|
13
|
+
* absent key, decided before the filesystem is touched.
|
|
14
|
+
*/
|
|
15
|
+
export class FileCompileCache {
|
|
16
|
+
directory;
|
|
17
|
+
fills = new Map();
|
|
18
|
+
lockPollMs;
|
|
19
|
+
lockTimeoutMs;
|
|
20
|
+
policy;
|
|
21
|
+
rule;
|
|
22
|
+
journal;
|
|
23
|
+
constructor(directory, options) {
|
|
24
|
+
this.directory = directory;
|
|
25
|
+
this.lockPollMs = this.positiveInteger(options.lockPollMs ?? DEFAULT_LOCK_POLL_MS, 'lockPollMs');
|
|
26
|
+
this.lockTimeoutMs = this.positiveInteger(options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS, 'lockTimeoutMs');
|
|
27
|
+
this.rule = compileCacheLifecycleRule(options.lifecyclePolicy);
|
|
28
|
+
this.policy = structuredClone(options.lifecyclePolicy);
|
|
29
|
+
this.journal = options.lifecycleJournal;
|
|
30
|
+
}
|
|
31
|
+
async getOrCompile(identity, context, ttlMs, compile, options = {}) {
|
|
32
|
+
validateCompileIdentity(identity);
|
|
33
|
+
const key = compileCacheKey(identity);
|
|
34
|
+
if (!compileCacheScopeAllowed(identity, context))
|
|
35
|
+
throw new CompileCacheRejectedError();
|
|
36
|
+
if (options.bypass)
|
|
37
|
+
return { outcome: 'bypass', key, entry: await buildCompileCacheEntry(identity, key, context, ttlMs, this.rule, compile) };
|
|
38
|
+
const existing = await this.readExisting(identity, context, true);
|
|
39
|
+
if (existing)
|
|
40
|
+
return { outcome: 'hit', key, entry: existing };
|
|
41
|
+
const active = this.fills.get(key);
|
|
42
|
+
if (active)
|
|
43
|
+
return { outcome: 'hit', key, entry: revalidateCompileCacheEntry(await active, identity, context) };
|
|
44
|
+
const fill = this.buildWithFileLock(identity, key, context, ttlMs, compile);
|
|
45
|
+
const shared = fill.then(result => result.entry);
|
|
46
|
+
void shared.catch(() => undefined);
|
|
47
|
+
this.fills.set(key, shared);
|
|
48
|
+
try {
|
|
49
|
+
const result = await fill;
|
|
50
|
+
return { outcome: result.filled ? 'miss' : 'hit', key, entry: structuredClone(result.entry) };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.fills.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async read(identity, context) {
|
|
57
|
+
if (!compileCacheScopeAllowed(identity, context))
|
|
58
|
+
return null;
|
|
59
|
+
return this.readExisting(identity, context, false);
|
|
60
|
+
}
|
|
61
|
+
async readExisting(identity, context, refreshExpired) {
|
|
62
|
+
// Validate integrity, identity, scope and authorization before deciding
|
|
63
|
+
// whether an expired entry can be treated as a cache miss for a fill.
|
|
64
|
+
const record = await this.readRecord(compileCacheKey(identity));
|
|
65
|
+
if (!record)
|
|
66
|
+
return null;
|
|
67
|
+
if (isTombstone(record))
|
|
68
|
+
throw new CompileCacheRejectedError();
|
|
69
|
+
const entry = revalidateCompileCacheEntry(record, identity, context, true);
|
|
70
|
+
if (context.nowEpochMs >= entry.expiresAtEpochMs) {
|
|
71
|
+
if (refreshExpired)
|
|
72
|
+
return null;
|
|
73
|
+
throw new CompileCacheRejectedError();
|
|
74
|
+
}
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
/** Erase the body and leave a persistent tombstone; the key never fills or restores again. */
|
|
78
|
+
async tombstone(identity, context) {
|
|
79
|
+
return this.retire(identity, context);
|
|
80
|
+
}
|
|
81
|
+
/** D10 deletion: the same body-free tombstone; false when absent, held or already erased. */
|
|
82
|
+
async delete(identity, context) {
|
|
83
|
+
return this.retire(identity, context);
|
|
84
|
+
}
|
|
85
|
+
/** Place (or with `null`, release) a D10 hold scoped to the `cache` surface. */
|
|
86
|
+
async setLegalHold(identity, context, hold) {
|
|
87
|
+
const entry = await this.readLifecycleEntry(identity, context);
|
|
88
|
+
if (entry)
|
|
89
|
+
await this.publish({ ...entry, legalHold: hold === null ? null : validateCompileCacheHold(hold, context.nowEpochMs) });
|
|
90
|
+
}
|
|
91
|
+
async retire(identity, context) {
|
|
92
|
+
// Retirement works after expiry, but still authenticates the stored
|
|
93
|
+
// identity and integrity before erasing anything.
|
|
94
|
+
const entry = await this.readLifecycleEntry(identity, context);
|
|
95
|
+
if (!entry || activeCompileCacheHold(entry, context.nowEpochMs))
|
|
96
|
+
return false;
|
|
97
|
+
const record = compileCacheTombstoneRecord(entry.key, context.nowEpochMs);
|
|
98
|
+
// Journal first so a failed local write cannot leave a restorable gap.
|
|
99
|
+
if (this.journal)
|
|
100
|
+
await this.journal.tombstone(structuredClone(record.tombstone));
|
|
101
|
+
await this.publish(record);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/** Authorized live or expired entry; null when absent, out of scope or already tombstoned. */
|
|
105
|
+
async readLifecycleEntry(identity, context) {
|
|
106
|
+
if (!compileCacheScopeAllowed(identity, context))
|
|
107
|
+
return null;
|
|
108
|
+
const record = await this.readRecord(compileCacheKey(identity));
|
|
109
|
+
if (!record || isTombstone(record))
|
|
110
|
+
return null;
|
|
111
|
+
return revalidateCompileCacheEntry(record, identity, context, true);
|
|
112
|
+
}
|
|
113
|
+
async readRecord(key) {
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = await readFile(this.path(key), 'utf8');
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code === 'ENOENT')
|
|
120
|
+
return null;
|
|
121
|
+
throw new CompileCacheRejectedError();
|
|
122
|
+
}
|
|
123
|
+
let record;
|
|
124
|
+
try {
|
|
125
|
+
record = JSON.parse(raw);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new CompileCacheRejectedError();
|
|
129
|
+
}
|
|
130
|
+
if (record?.schemaVersion === 'decision-compile-cache-tombstone/v1' && !isCompileCacheTombstone(record, key)) {
|
|
131
|
+
throw new CompileCacheRejectedError();
|
|
132
|
+
}
|
|
133
|
+
return record;
|
|
134
|
+
}
|
|
135
|
+
/** Export is explicit, scope-authorized, and allowed only when the lifecycle rule persists backups. */
|
|
136
|
+
async backup(identity, context) {
|
|
137
|
+
if (this.rule.backup === 'not-persisted')
|
|
138
|
+
throw new CompileCacheRejectedError();
|
|
139
|
+
return this.read(identity, context);
|
|
140
|
+
}
|
|
141
|
+
async restore(entry, context) {
|
|
142
|
+
// Restored backups must still be live at the point of import; old copies
|
|
143
|
+
// cannot resurrect deleted/tombstoned or expired compiler artifacts.
|
|
144
|
+
const restored = revalidateCompileCacheEntry(entry, entry.identity, context);
|
|
145
|
+
const existing = await this.readRecord(restored.key);
|
|
146
|
+
// A backup cannot replace an existing immutable entry or undo any tombstone,
|
|
147
|
+
// whether it is kept locally or in the independent lifecycle journal.
|
|
148
|
+
if (existing && !isTombstone(existing))
|
|
149
|
+
throw new CompileCacheRejectedError();
|
|
150
|
+
const tombstones = existing ? [existing.tombstone] : [];
|
|
151
|
+
if (this.journal) {
|
|
152
|
+
try {
|
|
153
|
+
tombstones.push(...await this.journal.tombstones(compileCacheReference(restored.key).opaqueId));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new CompileCacheRejectedError();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!mayRestoreCompileCacheEntry(restored, context.nowEpochMs, this.policy, tombstones))
|
|
160
|
+
throw new CompileCacheRejectedError();
|
|
161
|
+
await this.publish(restored);
|
|
162
|
+
}
|
|
163
|
+
async buildWithFileLock(identity, key, context, ttlMs, compile) {
|
|
164
|
+
const owner = await this.acquireFillLock(key);
|
|
165
|
+
try {
|
|
166
|
+
const existing = await this.readExisting(identity, context, true);
|
|
167
|
+
if (existing)
|
|
168
|
+
return { entry: existing, filled: false };
|
|
169
|
+
const entry = await buildCompileCacheEntry(identity, key, context, ttlMs, this.rule, compile);
|
|
170
|
+
await this.publish(entry);
|
|
171
|
+
return { entry, filled: true };
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await this.releaseFillLock(key, owner);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async acquireFillLock(key) {
|
|
178
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
179
|
+
const owner = { version: 1, pid: process.pid, token: randomUUID(), createdAtEpochMs: Date.now() };
|
|
180
|
+
const path = this.lockPath(key);
|
|
181
|
+
const started = Date.now();
|
|
182
|
+
while (true) {
|
|
183
|
+
try {
|
|
184
|
+
const handle = await open(path, 'wx', 0o600);
|
|
185
|
+
try {
|
|
186
|
+
await handle.writeFile(canonicalJson(owner), 'utf8');
|
|
187
|
+
await handle.sync();
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await handle.close();
|
|
191
|
+
}
|
|
192
|
+
return owner;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error.code !== 'EEXIST')
|
|
196
|
+
throw error;
|
|
197
|
+
if (!await this.lockOwnerAlive(path)) {
|
|
198
|
+
await unlink(path).catch(unlinkError => {
|
|
199
|
+
if (unlinkError.code !== 'ENOENT')
|
|
200
|
+
throw unlinkError;
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (Date.now() - started >= this.lockTimeoutMs)
|
|
205
|
+
throw new CompileCacheRejectedError();
|
|
206
|
+
await new Promise(resolve => setTimeout(resolve, this.lockPollMs));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async lockOwnerAlive(path) {
|
|
211
|
+
try {
|
|
212
|
+
const owner = JSON.parse(await readFile(path, 'utf8'));
|
|
213
|
+
if (owner.version !== 1 || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || typeof owner.token !== 'string') {
|
|
214
|
+
return this.lockRecordRecentlyCreated(path);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
process.kill(owner.pid, 0);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return error.code !== 'ESRCH';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error.code === 'ENOENT')
|
|
226
|
+
return true;
|
|
227
|
+
return this.lockRecordRecentlyCreated(path);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async lockRecordRecentlyCreated(path) {
|
|
231
|
+
try {
|
|
232
|
+
// Exclusive creation precedes the synced owner record. Both truncated and
|
|
233
|
+
// parseable-but-incomplete metadata must remain owned during that window.
|
|
234
|
+
return Date.now() - (await stat(path)).mtimeMs < Math.max(DEFAULT_LOCK_TIMEOUT_MS, this.lockTimeoutMs * 2);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return error.code === 'ENOENT';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async releaseFillLock(key, owner) {
|
|
241
|
+
const path = this.lockPath(key);
|
|
242
|
+
try {
|
|
243
|
+
const current = JSON.parse(await readFile(path, 'utf8'));
|
|
244
|
+
if (current.token === owner.token && current.pid === owner.pid)
|
|
245
|
+
await unlink(path);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error.code !== 'ENOENT')
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
positiveInteger(value, label) {
|
|
253
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
254
|
+
throw new TypeError(`${label} must be a positive integer`);
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
path(key) { return join(this.directory, `${key.slice('sha256:'.length)}.json`); }
|
|
258
|
+
lockPath(key) { return join(this.directory, `${key.slice('sha256:'.length)}.lock`); }
|
|
259
|
+
async publish(entry) {
|
|
260
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
261
|
+
const path = this.path(entry.key);
|
|
262
|
+
const temporary = join(dirname(path), `.compile-cache-${randomUUID()}.tmp`);
|
|
263
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
264
|
+
try {
|
|
265
|
+
await handle.writeFile(canonicalJson(entry), 'utf8');
|
|
266
|
+
await handle.sync();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await handle.close();
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
await rename(temporary, path);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
await unlink(temporary).catch(() => undefined);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=file-store.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
3
|
+
export function sha256(value) {
|
|
4
|
+
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
5
|
+
}
|
|
6
|
+
export function compileCacheKey(identity) {
|
|
7
|
+
validateCompileIdentity(identity);
|
|
8
|
+
return sha256(identity);
|
|
9
|
+
}
|
|
10
|
+
export function providerPrefixKey(identity) {
|
|
11
|
+
if (identity.policy.ttlMs <= 0 || !Number.isSafeInteger(identity.policy.ttlMs))
|
|
12
|
+
throw new Error('invalid provider prefix TTL');
|
|
13
|
+
return sha256(identity);
|
|
14
|
+
}
|
|
15
|
+
export function validateCompileIdentity(identity) {
|
|
16
|
+
if (!identity.sourceArtifactDigests.length || identity.sourceArtifactDigests.some(value => !/^sha256:[0-9a-f]{64}$/.test(value))) {
|
|
17
|
+
throw new Error('compile identity requires valid pinned source digests');
|
|
18
|
+
}
|
|
19
|
+
const required = [identity.compiler.id, identity.compiler.version, identity.runtimeVersion, identity.schemaVersion,
|
|
20
|
+
identity.canonicalizer.id, identity.canonicalizer.version, identity.adapter.id, identity.adapter.version,
|
|
21
|
+
identity.adapter.promptVersion, identity.backendCapabilityMode, identity.modelPolicy.requested,
|
|
22
|
+
identity.tenantId, identity.projectId, identity.dataClass];
|
|
23
|
+
if (required.some(value => !value))
|
|
24
|
+
throw new Error('compile identity contains an empty semantic dimension');
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './identity.js';
|
|
3
|
+
export * from './store.js';
|
|
4
|
+
export * from './file-store.js';
|
|
5
|
+
export * from './prefix.js';
|
|
6
|
+
export * from './benchmark.js';
|
|
7
|
+
export * from './runtime.js';
|
|
8
|
+
export * from './compatibility.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { providerPrefixKey } from './identity.js';
|
|
2
|
+
/** Provider status is derived exclusively from an explicit report/capability declaration. */
|
|
3
|
+
export function providerPrefixEvidence(identity, report, observedAtEpochMs = Date.now()) {
|
|
4
|
+
const base = { schemaVersion: 'decision-provider-prefix-evidence/v1', identityDigest: providerPrefixKey(identity) };
|
|
5
|
+
if (report.kind === 'reported' && validReportedEvidence(report)
|
|
6
|
+
&& Number.isSafeInteger(observedAtEpochMs) && observedAtEpochMs >= 0
|
|
7
|
+
&& (report.expiresAtEpochMs === null || report.expiresAtEpochMs > observedAtEpochMs))
|
|
8
|
+
return { ...base, status: report.hit ? 'hit' : 'miss', source: 'provider-report',
|
|
9
|
+
cacheVersion: report.cacheVersion, savedInputTokens: report.savedInputTokens, expiresAtEpochMs: report.expiresAtEpochMs };
|
|
10
|
+
if (report.kind === 'unsupported')
|
|
11
|
+
return { ...base, status: 'unsupported', source: 'documented-unsupported', cacheVersion: null, savedInputTokens: null, expiresAtEpochMs: null };
|
|
12
|
+
if (report.kind === 'bypass')
|
|
13
|
+
return { ...base, status: 'bypass', source: 'policy-bypass', cacheVersion: null, savedInputTokens: null, expiresAtEpochMs: null };
|
|
14
|
+
return { ...base, status: 'unknown', source: 'unreported', cacheVersion: null, savedInputTokens: null, expiresAtEpochMs: null };
|
|
15
|
+
}
|
|
16
|
+
function validReportedEvidence(report) {
|
|
17
|
+
return report.cacheVersion.length > 0 && report.cacheVersion.length <= 128
|
|
18
|
+
&& (report.savedInputTokens === null || (Number.isSafeInteger(report.savedInputTokens) && report.savedInputTokens >= 0))
|
|
19
|
+
&& (report.expiresAtEpochMs === null || (Number.isSafeInteger(report.expiresAtEpochMs) && report.expiresAtEpochMs >= 0));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=prefix.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
2
|
+
import { DECISION_LIFECYCLE_SURFACES, mayRestoreDecisionReference, validateDecisionLifecyclePolicy, } from '../lifecycle.js';
|
|
3
|
+
import { compileCacheKey, sha256 } from './identity.js';
|
|
4
|
+
export class CompileCacheRejectedError extends Error {
|
|
5
|
+
constructor() { super('compile cache entry unavailable'); this.name = 'CompileCacheRejectedError'; }
|
|
6
|
+
}
|
|
7
|
+
/** The D10 `cache` surface rule that governs retention, backup and hold handling. */
|
|
8
|
+
export function compileCacheLifecycleRule(policy) {
|
|
9
|
+
validateDecisionLifecyclePolicy(policy);
|
|
10
|
+
return structuredClone(policy.surfaces.cache);
|
|
11
|
+
}
|
|
12
|
+
export function compileCacheReference(key) {
|
|
13
|
+
return { surface: 'cache', opaqueId: key.slice('sha256:'.length) };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Scope and authorization are decided on the requested identity before any
|
|
17
|
+
* storage lookup, so a denied caller cannot tell a present entry from an absent one.
|
|
18
|
+
*/
|
|
19
|
+
export function compileCacheScopeAllowed(identity, context) {
|
|
20
|
+
try {
|
|
21
|
+
return identity.tenantId === context.tenantId && identity.projectId === context.projectId
|
|
22
|
+
&& context.authorize(identity) === true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function activeCompileCacheHold(entry, nowEpochMs) {
|
|
29
|
+
return entry.legalHold !== null && entry.legalHold.expiresAt > nowEpochMs;
|
|
30
|
+
}
|
|
31
|
+
export function validateCompileCacheHold(hold, nowEpochMs) {
|
|
32
|
+
if (!hold?.subject || !hold.reason || !hold.authorizedBy || !Array.isArray(hold.scope) || !hold.scope.includes('cache')
|
|
33
|
+
|| hold.scope.some(surface => !DECISION_LIFECYCLE_SURFACES.includes(surface))
|
|
34
|
+
|| !Number.isSafeInteger(hold.expiresAt) || hold.expiresAt <= nowEpochMs)
|
|
35
|
+
throw new Error('Decision lifecycle hold denied');
|
|
36
|
+
return structuredClone(hold);
|
|
37
|
+
}
|
|
38
|
+
export function compileCacheTombstoneRecord(key, nowEpochMs) {
|
|
39
|
+
const reference = compileCacheReference(key);
|
|
40
|
+
return { schemaVersion: 'decision-compile-cache-tombstone/v1', key,
|
|
41
|
+
tombstone: { subject: reference.opaqueId, reference, deletedAt: nowEpochMs } };
|
|
42
|
+
}
|
|
43
|
+
export function isCompileCacheTombstone(record, key) {
|
|
44
|
+
const value = record;
|
|
45
|
+
const reference = compileCacheReference(key);
|
|
46
|
+
return value?.schemaVersion === 'decision-compile-cache-tombstone/v1' && value.key === key
|
|
47
|
+
&& Object.keys(value).length === 3 && value.tombstone?.subject === reference.opaqueId
|
|
48
|
+
&& value.tombstone.reference?.surface === 'cache' && value.tombstone.reference.opaqueId === reference.opaqueId
|
|
49
|
+
&& Number.isSafeInteger(value.tombstone.deletedAt) && value.tombstone.deletedAt >= 0;
|
|
50
|
+
}
|
|
51
|
+
export async function buildCompileCacheEntry(identity, key, context, ttlMs, rule, compile) {
|
|
52
|
+
// Entry lifetime can never exceed the lifecycle retention for the cache surface.
|
|
53
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > rule.retentionMs
|
|
54
|
+
|| !compileCacheScopeAllowed(identity, context))
|
|
55
|
+
throw new CompileCacheRejectedError();
|
|
56
|
+
const value = await compile();
|
|
57
|
+
if (value === undefined || canonicalJson(value) === undefined)
|
|
58
|
+
throw new Error('compiler returned malformed output');
|
|
59
|
+
return { schemaVersion: 'decision-compile-cache-entry/v1', key, identity: structuredClone(identity),
|
|
60
|
+
value: structuredClone(value), valueDigest: sha256(value), createdAtEpochMs: context.nowEpochMs,
|
|
61
|
+
expiresAtEpochMs: context.nowEpochMs + ttlMs, legalHold: null };
|
|
62
|
+
}
|
|
63
|
+
/** Revalidate integrity, identity, scope, authorization and (unless allowed) expiry. */
|
|
64
|
+
export function revalidateCompileCacheEntry(entry, identity, context, allowExpired = false) {
|
|
65
|
+
let valid = false;
|
|
66
|
+
try {
|
|
67
|
+
valid = entry?.schemaVersion === 'decision-compile-cache-entry/v1'
|
|
68
|
+
&& entry.key === compileCacheKey(entry.identity) && entry.key === compileCacheKey(identity)
|
|
69
|
+
&& canonicalJson(entry.identity) === canonicalJson(identity) && entry.valueDigest === sha256(entry.value)
|
|
70
|
+
&& Number.isSafeInteger(entry.createdAtEpochMs) && Number.isSafeInteger(entry.expiresAtEpochMs)
|
|
71
|
+
&& (entry.legalHold === null || (typeof entry.legalHold === 'object' && Number.isSafeInteger(entry.legalHold.expiresAt)))
|
|
72
|
+
&& compileCacheScopeAllowed(entry.identity, context)
|
|
73
|
+
&& (allowExpired || context.nowEpochMs < entry.expiresAtEpochMs);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
valid = false;
|
|
77
|
+
}
|
|
78
|
+
if (!valid)
|
|
79
|
+
throw new CompileCacheRejectedError();
|
|
80
|
+
return structuredClone(entry);
|
|
81
|
+
}
|
|
82
|
+
/** D10 restore gate: any tombstone for the key, or elapsed retention, refuses the backup. */
|
|
83
|
+
export function mayRestoreCompileCacheEntry(entry, nowEpochMs, policy, tombstones) {
|
|
84
|
+
return mayRestoreDecisionReference(compileCacheReference(entry.key), entry.createdAtEpochMs, nowEpochMs, policy, tombstones);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { sanitizeOpaqueValue } from '../telemetry/redaction.js';
|
|
2
|
+
const REASONS = {
|
|
3
|
+
hit: 'verified-hit', miss: 'cold-fill', bypass: 'cache-disabled', rejected: 'store-rejected',
|
|
4
|
+
};
|
|
5
|
+
/** Metadata-only compile-layer telemetry: no alias, key, identity or artifact content. */
|
|
6
|
+
export function compileCacheTelemetry(identity, outcome, preparationLatencyMs, expiresAtEpochMs) {
|
|
7
|
+
return { schemaVersion: 'decision-cache-telemetry/v1', layer: identity?.layer ?? 'definition-compilation', outcome,
|
|
8
|
+
reason: REASONS[outcome], version: identity ? sanitizeOpaqueValue(identity.compiler.version, 64) : null, savedTokens: null,
|
|
9
|
+
preparationLatencyMs: Math.max(0, preparationLatencyMs), expiresAtEpochMs,
|
|
10
|
+
invalidationReason: outcome === 'rejected' ? 'revalidation-failed' : null };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compile the stable adapter material through one semantic path. Caching only
|
|
14
|
+
* selects where that exact artifact comes from; it never changes the request
|
|
15
|
+
* passed to the compiler or permits stale fallback.
|
|
16
|
+
*/
|
|
17
|
+
export async function prepareCompiledArtifact(input) {
|
|
18
|
+
if (!input.adapter.compile)
|
|
19
|
+
return undefined;
|
|
20
|
+
const compile = async () => input.adapter.compile({
|
|
21
|
+
definition: structuredClone(input.definition),
|
|
22
|
+
target: structuredClone(input.target),
|
|
23
|
+
});
|
|
24
|
+
const policy = input.policy;
|
|
25
|
+
if (!policy)
|
|
26
|
+
return compile();
|
|
27
|
+
const started = performance.now();
|
|
28
|
+
const emit = (identity, outcome, expiresAtEpochMs) => {
|
|
29
|
+
try {
|
|
30
|
+
policy.onTelemetry?.(compileCacheTelemetry(identity, outcome, performance.now() - started, expiresAtEpochMs));
|
|
31
|
+
}
|
|
32
|
+
catch { /* telemetry is observability-only */ }
|
|
33
|
+
};
|
|
34
|
+
if (!policy.enabled) {
|
|
35
|
+
const value = await compile();
|
|
36
|
+
emit(null, 'bypass', null);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
const identity = policy.identityFor(input);
|
|
40
|
+
const context = typeof policy.context === 'function' ? policy.context() : policy.context;
|
|
41
|
+
try {
|
|
42
|
+
const result = await policy.store.getOrCompile(identity, context, policy.ttlMs, compile);
|
|
43
|
+
policy.onResult?.({ alias: input.alias, outcome: result.outcome });
|
|
44
|
+
emit(identity, result.outcome, result.entry.expiresAtEpochMs);
|
|
45
|
+
return structuredClone(result.entry.value);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (policy.failureMode === 'fail') {
|
|
49
|
+
emit(identity, 'rejected', null);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
policy.onResult?.({ alias: input.alias, outcome: 'rejected' });
|
|
53
|
+
const value = await compile();
|
|
54
|
+
emit(identity, 'rejected', null);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function prepareAdapterRequest(request, adapter, policy) {
|
|
59
|
+
const compiledArtifact = await prepareCompiledArtifact({
|
|
60
|
+
alias: request.alias,
|
|
61
|
+
definition: request.definition,
|
|
62
|
+
target: request.target,
|
|
63
|
+
adapter,
|
|
64
|
+
policy,
|
|
65
|
+
});
|
|
66
|
+
return compiledArtifact === undefined ? request : { ...request, compiledArtifact };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { compileCacheKey, validateCompileIdentity } from './identity.js';
|
|
2
|
+
import { activeCompileCacheHold, buildCompileCacheEntry, compileCacheLifecycleRule, compileCacheScopeAllowed, compileCacheTombstoneRecord, CompileCacheRejectedError, mayRestoreCompileCacheEntry, revalidateCompileCacheEntry, validateCompileCacheHold, } from './records.js';
|
|
3
|
+
export { CompileCacheRejectedError } from './records.js';
|
|
4
|
+
const isTombstone = (record) => record.schemaVersion === 'decision-compile-cache-tombstone/v1';
|
|
5
|
+
/**
|
|
6
|
+
* In-memory reference store. Entries are immutable and concurrent fills share one promise.
|
|
7
|
+
* Out-of-scope or unauthorized lifecycle calls return the same result as an absent key.
|
|
8
|
+
*/
|
|
9
|
+
export class MemoryCompileCache {
|
|
10
|
+
records = new Map();
|
|
11
|
+
fills = new Map();
|
|
12
|
+
policy;
|
|
13
|
+
rule;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.rule = compileCacheLifecycleRule(options.lifecyclePolicy);
|
|
16
|
+
this.policy = structuredClone(options.lifecyclePolicy);
|
|
17
|
+
}
|
|
18
|
+
async getOrCompile(identity, context, ttlMs, compile, options = {}) {
|
|
19
|
+
validateCompileIdentity(identity);
|
|
20
|
+
const key = compileCacheKey(identity);
|
|
21
|
+
if (!compileCacheScopeAllowed(identity, context))
|
|
22
|
+
throw new CompileCacheRejectedError();
|
|
23
|
+
if (options.bypass)
|
|
24
|
+
return { outcome: 'bypass', key, entry: await buildCompileCacheEntry(identity, key, context, ttlMs, this.rule, compile) };
|
|
25
|
+
const existing = this.records.get(key);
|
|
26
|
+
if (existing) {
|
|
27
|
+
// Authenticate the old entry before allowing an expiry refresh. A
|
|
28
|
+
// tombstone or tamper is never silently converted into a cache miss.
|
|
29
|
+
if (isTombstone(existing))
|
|
30
|
+
throw new CompileCacheRejectedError();
|
|
31
|
+
revalidateCompileCacheEntry(existing, identity, context, true);
|
|
32
|
+
if (context.nowEpochMs < existing.expiresAtEpochMs) {
|
|
33
|
+
return { outcome: 'hit', key, entry: revalidateCompileCacheEntry(existing, identity, context) };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const active = this.fills.get(key);
|
|
37
|
+
if (active)
|
|
38
|
+
return { outcome: 'hit', key, entry: revalidateCompileCacheEntry(await active, identity, context) };
|
|
39
|
+
const fill = buildCompileCacheEntry(identity, key, context, ttlMs, this.rule, compile);
|
|
40
|
+
this.fills.set(key, fill);
|
|
41
|
+
try {
|
|
42
|
+
const entry = await fill;
|
|
43
|
+
if (this.records.get(key) && isTombstone(this.records.get(key)))
|
|
44
|
+
throw new CompileCacheRejectedError();
|
|
45
|
+
this.records.set(key, entry);
|
|
46
|
+
return { outcome: 'miss', key, entry: structuredClone(entry) };
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
this.fills.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
read(identity, context) {
|
|
53
|
+
const record = this.lookup(identity, context);
|
|
54
|
+
if (!record)
|
|
55
|
+
return null;
|
|
56
|
+
if (isTombstone(record))
|
|
57
|
+
throw new CompileCacheRejectedError();
|
|
58
|
+
return revalidateCompileCacheEntry(record, identity, context);
|
|
59
|
+
}
|
|
60
|
+
/** Erase the body and leave a persistent tombstone; the key never fills or restores again. */
|
|
61
|
+
tombstone(identity, context) {
|
|
62
|
+
return this.retire(identity, context);
|
|
63
|
+
}
|
|
64
|
+
/** D10 deletion: same body-free tombstone as `tombstone`; false when absent, held or already erased. */
|
|
65
|
+
delete(identity, context) {
|
|
66
|
+
return this.retire(identity, context);
|
|
67
|
+
}
|
|
68
|
+
/** Place (or with `null`, release) a D10 hold scoped to the `cache` surface. */
|
|
69
|
+
setLegalHold(identity, context, hold) {
|
|
70
|
+
const record = this.lookup(identity, context);
|
|
71
|
+
if (!record)
|
|
72
|
+
return;
|
|
73
|
+
if (isTombstone(record))
|
|
74
|
+
throw new CompileCacheRejectedError();
|
|
75
|
+
const entry = revalidateCompileCacheEntry(record, identity, context, true);
|
|
76
|
+
this.records.set(entry.key, { ...entry, legalHold: hold === null ? null : validateCompileCacheHold(hold, context.nowEpochMs) });
|
|
77
|
+
}
|
|
78
|
+
/** Export an authorized live entry when the lifecycle rule persists backups. */
|
|
79
|
+
backup(identity, context) {
|
|
80
|
+
if (this.rule.backup === 'not-persisted')
|
|
81
|
+
throw new CompileCacheRejectedError();
|
|
82
|
+
return this.read(identity, context);
|
|
83
|
+
}
|
|
84
|
+
/** Import a backup only while live, in retention, and never over a tombstone or live entry. */
|
|
85
|
+
restore(entry, context) {
|
|
86
|
+
const restored = revalidateCompileCacheEntry(entry, entry.identity, context);
|
|
87
|
+
const existing = this.records.get(restored.key);
|
|
88
|
+
if ((existing && !isTombstone(existing)) || !mayRestoreCompileCacheEntry(restored, context.nowEpochMs, this.policy, existing ? [existing.tombstone] : []))
|
|
89
|
+
throw new CompileCacheRejectedError();
|
|
90
|
+
this.records.set(restored.key, restored);
|
|
91
|
+
}
|
|
92
|
+
lookup(identity, context) {
|
|
93
|
+
if (!compileCacheScopeAllowed(identity, context))
|
|
94
|
+
return null;
|
|
95
|
+
return this.records.get(compileCacheKey(identity)) ?? null;
|
|
96
|
+
}
|
|
97
|
+
retire(identity, context) {
|
|
98
|
+
const record = this.lookup(identity, context);
|
|
99
|
+
if (!record || isTombstone(record))
|
|
100
|
+
return false;
|
|
101
|
+
const entry = revalidateCompileCacheEntry(record, identity, context, true);
|
|
102
|
+
if (activeCompileCacheHold(entry, context.nowEpochMs))
|
|
103
|
+
return false;
|
|
104
|
+
this.records.set(entry.key, compileCacheTombstoneRecord(entry.key, context.nowEpochMs));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=store.js.map
|