@cassiomc1/forgeloop 1.9.0 → 1.10.1
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/AGENT_COMPATIBILITY.md +15 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +13 -2
- package/LOOP_ENGINEERING.md +17 -4
- package/LOOP_SYSTEM_DESIGN.md +56 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +31 -0
- package/QUALITY_SCORECARD.md +2 -2
- package/README.md +53 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +24 -0
- package/completions/_forgeloop +2 -1
- package/completions/forgeloop.bash +3 -1
- package/completions/forgeloop.fish +9 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +28 -2
- package/docs/ARTIFACT_REFERENCE.md +29 -0
- package/docs/CLI_REFERENCE.md +40 -0
- package/docs/CODE_ATTESTATION.md +9 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +11 -4
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +10 -0
- package/docs/MCP.md +29 -1
- package/docs/PACKAGE_CONTENTS.md +83 -0
- package/docs/RECIPES.md +80 -0
- package/docs/RELEASE_CHECKLIST.md +22 -0
- package/docs/REVISION_PROVIDERS.md +9 -0
- package/docs/TROUBLESHOOTING.md +66 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
- package/docs/diagrams/README.md +17 -0
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/package.json +14 -4
- package/schemas/handoff-envelope.schema.json +1 -0
- package/scripts/CI_VALIDATORS.md +24 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/check-critical-coverage.mjs +9 -0
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +6 -0
- package/src/commands/doctor.js +11 -10
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/actions.js +2 -2
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/approvals.js +2 -2
- package/src/core/artifacts.js +3 -3
- package/src/core/checks.js +0 -33
- package/src/core/cli-command-definitions.js +17 -0
- package/src/core/command-executors.js +12 -0
- package/src/core/command-input.js +11 -1
- package/src/core/completion.js +2 -2
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +113 -0
- package/src/core/events.js +37 -5
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/execution-profile.js +18 -5
- package/src/core/filesystem.js +18 -2
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/integration-invocation-policy.js +19 -2
- package/src/core/integration-resources.js +21 -1
- package/src/core/next-action-pending-actions.js +255 -0
- package/src/core/next-action-phases.js +26 -764
- package/src/core/next-action-planned-phase.js +51 -0
- package/src/core/next-action-quality-guidance.js +19 -0
- package/src/core/next-action-recovery-phases.js +97 -0
- package/src/core/next-action-refresh.js +20 -0
- package/src/core/next-action-review-phase.js +189 -0
- package/src/core/next-action-verification-phase.js +192 -0
- package/src/core/portable-context.js +103 -0
- package/src/core/protocol-info.js +18 -2
- package/src/core/runtime-context.js +31 -0
- package/src/core/task-recovery.js +2 -2
- package/src/core/transaction-maintenance.js +70 -0
- package/src/core/transaction.js +31 -10
- package/src/core/work-state.js +5 -5
- package/src/integration.d.ts +135 -3
- package/src/integration.js +23 -0
- package/src/core/cli-metadata.js +0 -23
- package/src/core/decision-classification.js +0 -55
- package/src/core/gates.js +0 -57
- package/src/core/workflow-compatibility.js +0 -151
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
2
|
+
import { evaluateStartExecutionPrerequisites, PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE } from "./execution-prerequisites.js";
|
|
3
|
+
import { NEXT_ACTIONS, commandFor, decision, result } from "./next-action-model.js";
|
|
4
|
+
import { artifactError } from "./next-action-artifacts.js";
|
|
5
|
+
import { projectStructuralQualityStatus } from "./structural-quality/service.js";
|
|
6
|
+
import { structuralQualityOptionalActions } from "./next-action-quality-guidance.js";
|
|
7
|
+
|
|
8
|
+
export async function resolvePlannedPhase({ target, packageRoot, explicitTaskId, state, runtimeContext, context, preflightArtifact }) {
|
|
9
|
+
const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
|
|
10
|
+
if (quality.mode === "gate" && quality.baseline.status !== "OBSERVED") {
|
|
11
|
+
return result({
|
|
12
|
+
...context,
|
|
13
|
+
nextAction: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
|
|
14
|
+
commands: [commandFor(NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE).replace("<id>", state.taskId)],
|
|
15
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Gate mode requires a structural-quality baseline before execution", [quality.baseline.artifactRef ?? taskArtifactPath(state.taskId, "structuralQuality")])],
|
|
16
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
|
|
20
|
+
if (prerequisites.errors.length > 0) {
|
|
21
|
+
const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
|
|
22
|
+
|| (error.code === "E_PHASE_CHRONOLOGY_INVALID"
|
|
23
|
+
&& error.message === PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE));
|
|
24
|
+
if (preflightOnly) {
|
|
25
|
+
return result({
|
|
26
|
+
...context,
|
|
27
|
+
nextAction: NEXT_ACTIONS.RUN_PREFLIGHT,
|
|
28
|
+
reasons: prerequisites.errors,
|
|
29
|
+
commands: [commandFor(NEXT_ACTIONS.RUN_PREFLIGHT)],
|
|
30
|
+
requiredArtifacts: prerequisites.requiredArtifacts,
|
|
31
|
+
missingArtifacts: preflightArtifact.missingArtifacts,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const routeOnly = prerequisites.errors.every((error) => error.code === "E_ROUTE_STALE" || error.code === "E_ROUTE_GUIDE_MISMATCH");
|
|
35
|
+
return result({
|
|
36
|
+
...context,
|
|
37
|
+
nextAction: routeOnly ? NEXT_ACTIONS.RESOLVE_STALE_ROUTE : NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
38
|
+
reasons: prerequisites.errors,
|
|
39
|
+
requiredArtifacts: prerequisites.requiredArtifacts,
|
|
40
|
+
missingArtifacts: preflightArtifact.missingArtifacts,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return decision(
|
|
44
|
+
context,
|
|
45
|
+
NEXT_ACTIONS.START_EXECUTION,
|
|
46
|
+
artifactError("PHASE_PLANNED", "The persisted preflight is READY"),
|
|
47
|
+
[],
|
|
48
|
+
[],
|
|
49
|
+
structuralQualityOptionalActions(quality, state.taskId),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NEXT_ACTIONS } from "./next-action-model.js";
|
|
2
|
+
|
|
3
|
+
export function structuralQualityOptionalActions(quality, taskId) {
|
|
4
|
+
if (quality?.mode !== "observe") return [];
|
|
5
|
+
if (quality.baseline?.status !== "OBSERVED") {
|
|
6
|
+
return [{
|
|
7
|
+
action: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
|
|
8
|
+
command: `forgeloop quality-baseline --task ${taskId} --json`,
|
|
9
|
+
}];
|
|
10
|
+
}
|
|
11
|
+
if (quality.freshness === "STALE" || quality.current?.verificationCycle === null
|
|
12
|
+
|| quality.current?.verificationCycle === undefined) {
|
|
13
|
+
return [{
|
|
14
|
+
action: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
15
|
+
command: `forgeloop quality-verify --task ${taskId} --json`,
|
|
16
|
+
}];
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { validateEventLedger } from "./events.js";
|
|
2
|
+
import { NEXT_ACTIONS, commandFor, decision, recordDiagnosisCommandSpec, recordInterventionCommandSpec, result } from "./next-action-model.js";
|
|
3
|
+
import { artifactError } from "./next-action-artifacts.js";
|
|
4
|
+
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
5
|
+
import { buildTaskReflection } from "./reflection.js";
|
|
6
|
+
import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
|
|
7
|
+
|
|
8
|
+
export async function resolveDiagnosingPhase({ target, packageRoot, normalized, state, context, eventsRel, requiredArtifacts, stateRel }) {
|
|
9
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
|
|
10
|
+
const cycle = state.verificationCycle ?? 1;
|
|
11
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
12
|
+
if (!diagEvent) {
|
|
13
|
+
return result({
|
|
14
|
+
...context,
|
|
15
|
+
nextAction: NEXT_ACTIONS.RECORD_DIAGNOSIS,
|
|
16
|
+
reasons: [
|
|
17
|
+
artifactError(
|
|
18
|
+
"E_DIAGNOSIS_REQUIRED",
|
|
19
|
+
"Current correction cycle has no append-only diagnosis record.",
|
|
20
|
+
[eventsRel],
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
commandSpecs: [recordDiagnosisCommandSpec()],
|
|
24
|
+
requiredArtifacts: [...requiredArtifacts, eventsRel],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const progress = evaluateProgress({ state, events: ledger.events });
|
|
28
|
+
if (progress.status === PROGRESS_STATUS.STALLED) {
|
|
29
|
+
let oscillating = false;
|
|
30
|
+
try {
|
|
31
|
+
const reflection = await buildTaskReflection({ target, packageRoot, taskId: normalized.taskId ?? null });
|
|
32
|
+
oscillating = reflection.oscillation.detected;
|
|
33
|
+
} catch {
|
|
34
|
+
// Reflection is advisory guidance; stall handling must not depend on it.
|
|
35
|
+
}
|
|
36
|
+
return result({
|
|
37
|
+
...context,
|
|
38
|
+
nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
|
|
39
|
+
reasons: [
|
|
40
|
+
artifactError(
|
|
41
|
+
"E_PROGRESS_STALLED",
|
|
42
|
+
"The current correction strategy has no new diagnostic information.",
|
|
43
|
+
[stateRel, eventsRel],
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
progress,
|
|
47
|
+
diagnosticGuidance: {
|
|
48
|
+
action: oscillating ? NEXT_ACTIONS.INTRODUCE_NEW_OBSERVATION : NEXT_ACTIONS.REQUIRE_NEW_DIAGNOSTIC_INFORMATION,
|
|
49
|
+
signals: oscillating ? ["OSCILLATING_STRATEGY"] : ["NO_INFORMATION_GAIN", ...progress.signals.map((signal) => signal.code)],
|
|
50
|
+
errorCodes: oscillating ? ["E_STRATEGY_OSCILLATION", "E_PROGRESS_STALLED"] : ["E_PROGRESS_STALLED"],
|
|
51
|
+
},
|
|
52
|
+
requiredArtifacts,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return result({
|
|
56
|
+
...context,
|
|
57
|
+
nextAction: NEXT_ACTIONS.CORRECT,
|
|
58
|
+
commands: [commandFor(NEXT_ACTIONS.CORRECT)],
|
|
59
|
+
reasons: [
|
|
60
|
+
artifactError("PHASE_DIAGNOSING", "The persisted diagnosis hypothesis permits correction"),
|
|
61
|
+
],
|
|
62
|
+
...(progress.status === PROGRESS_STATUS.WATCH ? { progress } : {}),
|
|
63
|
+
requiredArtifacts,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveCorrectingPhase({ target, packageRoot, normalized, state, context, requiredArtifacts }) {
|
|
68
|
+
const ledgerForCorrection = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
|
|
69
|
+
const correctionCycle = state.verificationCycle ?? 1;
|
|
70
|
+
const hasStructuredCase = ledgerForCorrection.events.some(
|
|
71
|
+
(event) => event.event === "DIAGNOSTIC_CASE_RECORDED"
|
|
72
|
+
&& event.taskId === state.taskId
|
|
73
|
+
&& event.details?.verificationCycle === correctionCycle,
|
|
74
|
+
);
|
|
75
|
+
const hasIntervention = ledgerForCorrection.events.some(
|
|
76
|
+
(event) => event.event === "INTERVENTION_RECORDED"
|
|
77
|
+
&& event.taskId === state.taskId
|
|
78
|
+
&& event.details?.verificationCycle === correctionCycle,
|
|
79
|
+
);
|
|
80
|
+
if (hasStructuredCase && !hasIntervention) {
|
|
81
|
+
return result({
|
|
82
|
+
...context,
|
|
83
|
+
nextAction: NEXT_ACTIONS.ENTER_VERIFYING,
|
|
84
|
+
reasons: [
|
|
85
|
+
artifactError("PHASE_CORRECTING", "A structured diagnostic case is awaiting a recorded intervention"),
|
|
86
|
+
],
|
|
87
|
+
diagnosticGuidance: {
|
|
88
|
+
action: NEXT_ACTIONS.RECORD_INTERVENTION,
|
|
89
|
+
signals: ["INTERVENTION_NOT_BOUND_TO_HYPOTHESIS"],
|
|
90
|
+
errorCodes: [],
|
|
91
|
+
commandSpecs: [recordInterventionCommandSpec(normalized.taskId)],
|
|
92
|
+
},
|
|
93
|
+
requiredArtifacts,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { validateEventLedger } from "./events.js";
|
|
2
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
3
|
+
import { directCommandSpec } from "./next-action-model.js";
|
|
4
|
+
|
|
5
|
+
/** Offer checkpoint removal only before execution, with valid blocked chronology. */
|
|
6
|
+
export async function preExecutionRefreshGuidance({ target, packageRoot, state, contract }) {
|
|
7
|
+
if (!["ROUTED", "DESIGNING", "PLANNED"].includes(state.phase)) return {};
|
|
8
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: state.taskId });
|
|
9
|
+
if (!ledger.valid || ledger.events.some((entry) => entry.event === "EXECUTION_STARTED")) return {};
|
|
10
|
+
const lastPreflight = ledger.events.filter((entry) => ["PREFLIGHT_READY", "PREFLIGHT_BLOCKED"].includes(entry.event)).at(-1);
|
|
11
|
+
if (lastPreflight?.event !== "PREFLIGHT_BLOCKED") return {};
|
|
12
|
+
let route;
|
|
13
|
+
try { route = await readPersistedRoute(target, packageRoot, { taskId: state.taskId }); }
|
|
14
|
+
catch { return {}; }
|
|
15
|
+
if (route.value.contractFingerprint !== contract.fingerprint) return {};
|
|
16
|
+
return {
|
|
17
|
+
commands: [`forgeloop clear-state --task ${state.taskId}`, `forgeloop preflight --task ${state.taskId}`],
|
|
18
|
+
commandSpecs: [directCommandSpec("clear-state", state.taskId), directCommandSpec("preflight", state.taskId)],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { readJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { evaluateCompletion } from "./completion.js";
|
|
3
|
+
import { validateReceipt } from "./receipt.js";
|
|
4
|
+
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
5
|
+
import { evaluateRequiredEvidence, classifyRequirement } from "./evidence-readiness.js";
|
|
6
|
+
import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./events.js";
|
|
7
|
+
import { NEXT_ACTIONS, commandFor, decision, recordTerminalResultCommandSpec, result } from "./next-action-model.js";
|
|
8
|
+
import { artifactError, checkListReasons, loadArtifact, requirementsAndCoverage } from "./next-action-artifacts.js";
|
|
9
|
+
|
|
10
|
+
export async function resolveReviewingPhase({ state, context, requiredArtifacts, target, packageRoot, contract, route, preflight, authorityContext, runtimeContext, explicitTaskId, eventsRel, receiptRel, stateRel, policyRecoveryAction }) {
|
|
11
|
+
const invalidChecks = checkListReasons(state);
|
|
12
|
+
if (invalidChecks.length > 0) {
|
|
13
|
+
return result({
|
|
14
|
+
...context,
|
|
15
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
16
|
+
reasons: invalidChecks,
|
|
17
|
+
requiredArtifacts,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const evidence = await requirementsAndCoverage({
|
|
21
|
+
target,
|
|
22
|
+
packageRoot,
|
|
23
|
+
contract,
|
|
24
|
+
route,
|
|
25
|
+
checks: state.checks,
|
|
26
|
+
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
27
|
+
authorityContext,
|
|
28
|
+
runtimeContext,
|
|
29
|
+
});
|
|
30
|
+
const readiness = evaluateRequiredEvidence({
|
|
31
|
+
requirements: evidence.requirements,
|
|
32
|
+
checks: state.checks,
|
|
33
|
+
target,
|
|
34
|
+
taskId: contract?.value?.taskId,
|
|
35
|
+
options: { authorityContext, runtimeContext },
|
|
36
|
+
});
|
|
37
|
+
if (!readiness.ready) {
|
|
38
|
+
let recoveryAuthorized = false;
|
|
39
|
+
if (state.lastCompletionAttempt?.status === "REJECTED") {
|
|
40
|
+
try {
|
|
41
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: explicitTaskId, eventsPath: eventsRel });
|
|
42
|
+
let currentReceipt = null;
|
|
43
|
+
try {
|
|
44
|
+
const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
45
|
+
currentReceipt = receiptArtifact?.value;
|
|
46
|
+
} catch {}
|
|
47
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization({
|
|
48
|
+
state,
|
|
49
|
+
receipt: currentReceipt,
|
|
50
|
+
events: ledger.events,
|
|
51
|
+
});
|
|
52
|
+
recoveryAuthorized = recoveryAuth.authorized;
|
|
53
|
+
} catch {
|
|
54
|
+
recoveryAuthorized = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result({
|
|
58
|
+
...context,
|
|
59
|
+
nextAction: recoveryAuthorized
|
|
60
|
+
? NEXT_ACTIONS.ENTER_VERIFYING
|
|
61
|
+
: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
62
|
+
commands: recoveryAuthorized
|
|
63
|
+
? [commandFor(NEXT_ACTIONS.ENTER_VERIFYING)]
|
|
64
|
+
: [],
|
|
65
|
+
reasons: [...readiness.invalid, ...readiness.partial, ...readiness.missing]
|
|
66
|
+
.map((item) => artifactError(
|
|
67
|
+
readiness.missing.some((candidate) => candidate.id === item.id)
|
|
68
|
+
? "E_EVIDENCE_REQUIRED"
|
|
69
|
+
: item.reasonCode ?? "E_EVIDENCE_PARTIAL",
|
|
70
|
+
`Evidence is not ready: ${item.text}`,
|
|
71
|
+
[stateRel],
|
|
72
|
+
)),
|
|
73
|
+
requiredArtifacts,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const receipt = await loadArtifact(
|
|
77
|
+
() => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
|
|
78
|
+
receiptRel,
|
|
79
|
+
);
|
|
80
|
+
if (receipt.error) {
|
|
81
|
+
if (receipt.error.code !== "ARTIFACT_MISSING") {
|
|
82
|
+
return decision(
|
|
83
|
+
context,
|
|
84
|
+
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
85
|
+
artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
|
|
86
|
+
[...requiredArtifacts, receiptRel],
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return decision(
|
|
90
|
+
context,
|
|
91
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
92
|
+
artifactError(
|
|
93
|
+
receipt.error.code === "ARTIFACT_MISSING" ? "E_RECEIPT_MISSING" : "E_RECEIPT_INVALID",
|
|
94
|
+
receipt.error.message,
|
|
95
|
+
[receiptRel],
|
|
96
|
+
),
|
|
97
|
+
[...requiredArtifacts, receiptRel],
|
|
98
|
+
receipt.missingArtifacts,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await validateReceipt(receipt.value.value, packageRoot, {
|
|
103
|
+
target,
|
|
104
|
+
taskId: contract?.value?.taskId,
|
|
105
|
+
authorityContext,
|
|
106
|
+
runtimeContext,
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return decision(
|
|
110
|
+
context,
|
|
111
|
+
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
112
|
+
artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
|
|
113
|
+
[...requiredArtifacts, receiptRel],
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const receiptRelationships = completionRelationshipErrors({
|
|
117
|
+
contract,
|
|
118
|
+
route,
|
|
119
|
+
state,
|
|
120
|
+
receipt: receipt.value.value,
|
|
121
|
+
requiredEvidence: evidence.requirements,
|
|
122
|
+
target,
|
|
123
|
+
taskId: contract?.value?.taskId,
|
|
124
|
+
authorityContext,
|
|
125
|
+
runtimeContext,
|
|
126
|
+
});
|
|
127
|
+
if (receiptRelationships.length > 0) {
|
|
128
|
+
if (receiptRelationships.some((err) => (
|
|
129
|
+
err.code === "E_RECEIPT_STATE_MISMATCH"
|
|
130
|
+
|| err.code === "E_RECEIPT_CYCLE_MISMATCH"
|
|
131
|
+
|| err.code === "E_RECEIPT_CONTRACT_MISMATCH"
|
|
132
|
+
|| err.code === "E_ROUTE_GUIDE_MISMATCH"
|
|
133
|
+
|| err.code === "E_EVIDENCE_COVERAGE_INVALID"
|
|
134
|
+
))) {
|
|
135
|
+
return decision(
|
|
136
|
+
context,
|
|
137
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
138
|
+
artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
|
|
139
|
+
[...requiredArtifacts, receiptRel],
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return result({
|
|
143
|
+
...context,
|
|
144
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
145
|
+
reasons: receiptRelationships,
|
|
146
|
+
requiredArtifacts: [...requiredArtifacts, receiptRel],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const completion = await evaluateCompletion({ target, packageRoot, taskId: explicitTaskId, authorityContext, runtimeContext });
|
|
151
|
+
if (completion.status !== "VALID") {
|
|
152
|
+
const terminalPendingErrors = completion.errors.filter((err) => (
|
|
153
|
+
err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
|
|
154
|
+
));
|
|
155
|
+
if (terminalPendingErrors.length > 0 && terminalPendingErrors.length === completion.errors.length) {
|
|
156
|
+
const terminalPendingReqs = terminalPendingErrors.map((err) => {
|
|
157
|
+
const reqId = err.requirementId;
|
|
158
|
+
const matchingReq = evidence.requirements.find((r) => r.id === reqId)
|
|
159
|
+
?? evidence.requirements.find((r) => r.text === reqId)
|
|
160
|
+
?? classifyRequirement(reqId ?? err.message);
|
|
161
|
+
return matchingReq;
|
|
162
|
+
});
|
|
163
|
+
return result({
|
|
164
|
+
...context,
|
|
165
|
+
nextAction: NEXT_ACTIONS.RECORD_TERMINAL_RESULT,
|
|
166
|
+
commands: ["forgeloop record-terminal-result"],
|
|
167
|
+
commandSpecs: terminalPendingReqs.map(recordTerminalResultCommandSpec),
|
|
168
|
+
reasons: completion.errors,
|
|
169
|
+
requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const policyAction = policyRecoveryAction(completion.errors);
|
|
173
|
+
if (policyAction) {
|
|
174
|
+
return result({
|
|
175
|
+
...context,
|
|
176
|
+
nextAction: policyAction,
|
|
177
|
+
reasons: completion.errors,
|
|
178
|
+
requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return result({
|
|
182
|
+
...context,
|
|
183
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
184
|
+
reasons: completion.errors,
|
|
185
|
+
requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return decision(context, NEXT_ACTIONS.RUN_COMPLETE, artifactError("COMPLETION_READY", "Completion artifacts and cross-artifact validation are valid"));
|
|
189
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { readJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { taskArtifactPath, taskStructuralQualityDirectory } from "./task-paths.js";
|
|
3
|
+
import { validateReceipt } from "./receipt.js";
|
|
4
|
+
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
5
|
+
import { evaluateRequiredEvidence, authoritativeChecksForRequirements, ordinaryLeafRequirements } from "./evidence-readiness.js";
|
|
6
|
+
import { NEXT_ACTIONS, commandFor, decision, recordCheckCommandSpec, result } from "./next-action-model.js";
|
|
7
|
+
import { artifactError, checkListReasons, loadArtifact, requirementsAndCoverage } from "./next-action-artifacts.js";
|
|
8
|
+
import { projectStructuralQualityStatus } from "./structural-quality/service.js";
|
|
9
|
+
import { structuralQualityOptionalActions } from "./next-action-quality-guidance.js";
|
|
10
|
+
|
|
11
|
+
export async function resolveVerifyingPhase({ target, packageRoot, explicitTaskId, state, runtimeContext, context, requiredArtifacts, contract, route, preflight, authorityContext, stateRel, receiptRel }) {
|
|
12
|
+
const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
|
|
13
|
+
if (quality.mode === "gate") {
|
|
14
|
+
if (quality.baseline.status !== "OBSERVED") {
|
|
15
|
+
return result({
|
|
16
|
+
...context,
|
|
17
|
+
nextAction: NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER,
|
|
18
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Structural-quality gate cannot verify without its immutable baseline", [taskArtifactPath(state.taskId, "structuralQuality")])],
|
|
19
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (!quality.current.artifactRef || quality.current.verificationCycle !== (state.verificationCycle ?? 1)) {
|
|
23
|
+
return result({
|
|
24
|
+
...context,
|
|
25
|
+
nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
26
|
+
commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
|
|
27
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "The current verification cycle has no structural-quality evaluation", [taskStructuralQualityDirectory(state.taskId)])],
|
|
28
|
+
requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (quality.freshness === "STALE") {
|
|
32
|
+
return result({
|
|
33
|
+
...context,
|
|
34
|
+
nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
35
|
+
commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
|
|
36
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality evidence is stale; fresh verification is required before review", [quality.current.artifactRef])],
|
|
37
|
+
requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (quality.current.status === "FAIL") {
|
|
41
|
+
return decision(context, NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION, artifactError("E_STRUCTURAL_QUALITY_REGRESSION", "Structural-quality verification detected a regression", [quality.current.artifactRef]));
|
|
42
|
+
}
|
|
43
|
+
if (quality.current.status === "BLOCKED") {
|
|
44
|
+
return decision(context, NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER, artifactError("E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE", "Structural-quality verification is blocked", [quality.current.artifactRef]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const invalidChecks = checkListReasons(state);
|
|
48
|
+
if (invalidChecks.length > 0) {
|
|
49
|
+
return result({
|
|
50
|
+
...context,
|
|
51
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
52
|
+
reasons: invalidChecks,
|
|
53
|
+
requiredArtifacts,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const evidence = await requirementsAndCoverage({
|
|
57
|
+
target,
|
|
58
|
+
packageRoot,
|
|
59
|
+
contract,
|
|
60
|
+
route,
|
|
61
|
+
checks: state.checks,
|
|
62
|
+
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
63
|
+
authorityContext,
|
|
64
|
+
runtimeContext,
|
|
65
|
+
});
|
|
66
|
+
const authoritative = authoritativeChecksForRequirements({
|
|
67
|
+
requirements: ordinaryLeafRequirements(evidence.requirements),
|
|
68
|
+
checks: state.checks,
|
|
69
|
+
});
|
|
70
|
+
const failed = authoritative.find(({ check }) => check?.status === "failed");
|
|
71
|
+
if (failed) {
|
|
72
|
+
return decision(
|
|
73
|
+
context,
|
|
74
|
+
NEXT_ACTIONS.DIAGNOSE,
|
|
75
|
+
artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [stateRel]),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const blocked = authoritative.find(({ check }) => check?.status === "blocked");
|
|
79
|
+
if (blocked) {
|
|
80
|
+
return decision(
|
|
81
|
+
context,
|
|
82
|
+
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
83
|
+
artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [stateRel]),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const receipt = await loadArtifact(
|
|
87
|
+
() => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
|
|
88
|
+
receiptRel,
|
|
89
|
+
);
|
|
90
|
+
if (receipt.error) {
|
|
91
|
+
if (receipt.error.code === "ARTIFACT_MISSING") {
|
|
92
|
+
return decision(
|
|
93
|
+
context,
|
|
94
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
95
|
+
artifactError("E_RECEIPT_MISSING", "Prepare the execution receipt before recording verification checks", [receiptRel]),
|
|
96
|
+
[...requiredArtifacts, receiptRel],
|
|
97
|
+
receipt.missingArtifacts,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return decision(
|
|
101
|
+
context,
|
|
102
|
+
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
103
|
+
artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
|
|
104
|
+
[...requiredArtifacts, receiptRel],
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await validateReceipt(receipt.value.value, packageRoot, {
|
|
109
|
+
target,
|
|
110
|
+
taskId: contract?.value?.taskId,
|
|
111
|
+
authorityContext,
|
|
112
|
+
runtimeContext,
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return decision(
|
|
116
|
+
context,
|
|
117
|
+
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
118
|
+
artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
|
|
119
|
+
[...requiredArtifacts, receiptRel],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const readiness = evaluateRequiredEvidence({
|
|
123
|
+
requirements: evidence.requirements,
|
|
124
|
+
checks: state.checks,
|
|
125
|
+
target,
|
|
126
|
+
taskId: contract?.value?.taskId,
|
|
127
|
+
options: { authorityContext, runtimeContext },
|
|
128
|
+
});
|
|
129
|
+
const receiptRelationships = completionRelationshipErrors({
|
|
130
|
+
contract,
|
|
131
|
+
route,
|
|
132
|
+
state,
|
|
133
|
+
receipt: receipt.value.value,
|
|
134
|
+
requiredEvidence: evidence.requirements,
|
|
135
|
+
requireRequiredChecks: false,
|
|
136
|
+
target,
|
|
137
|
+
taskId: contract?.value?.taskId,
|
|
138
|
+
authorityContext,
|
|
139
|
+
runtimeContext,
|
|
140
|
+
});
|
|
141
|
+
if (receiptRelationships.length > 0) {
|
|
142
|
+
if (receiptRelationships.some((err) => (
|
|
143
|
+
err.code === "E_RECEIPT_STATE_MISMATCH"
|
|
144
|
+
|| err.code === "E_RECEIPT_CYCLE_MISMATCH"
|
|
145
|
+
|| err.code === "E_RECEIPT_CONTRACT_MISMATCH"
|
|
146
|
+
|| err.code === "E_ROUTE_GUIDE_MISMATCH"
|
|
147
|
+
|| err.code === "E_EVIDENCE_COVERAGE_INVALID"
|
|
148
|
+
))) {
|
|
149
|
+
return decision(
|
|
150
|
+
context,
|
|
151
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
152
|
+
artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
|
|
153
|
+
[...requiredArtifacts, receiptRel],
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return result({
|
|
157
|
+
...context,
|
|
158
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
159
|
+
reasons: receiptRelationships,
|
|
160
|
+
requiredArtifacts: [...requiredArtifacts, receiptRel],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (readiness.ready) {
|
|
164
|
+
return decision(
|
|
165
|
+
context,
|
|
166
|
+
NEXT_ACTIONS.ENTER_REVIEWING,
|
|
167
|
+
artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"),
|
|
168
|
+
[],
|
|
169
|
+
[],
|
|
170
|
+
structuralQualityOptionalActions(quality, state.taskId),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
|
|
174
|
+
return result({
|
|
175
|
+
...context,
|
|
176
|
+
nextAction: NEXT_ACTIONS.RECORD_VERIFICATION,
|
|
177
|
+
commands: ["forgeloop record-check"],
|
|
178
|
+
commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.text)),
|
|
179
|
+
reasons: uncovered
|
|
180
|
+
.map((item) => artifactError(
|
|
181
|
+
readiness.invalid.some((candidate) => candidate.id === item.id)
|
|
182
|
+
? "E_EVIDENCE_INVALID"
|
|
183
|
+
: readiness.partial.some((candidate) => candidate.id === item.id)
|
|
184
|
+
? "E_EVIDENCE_PARTIAL"
|
|
185
|
+
: "E_EVIDENCE_REQUIRED",
|
|
186
|
+
"Run the required check and record observed evidence through the structured command specification.",
|
|
187
|
+
[stateRel],
|
|
188
|
+
)),
|
|
189
|
+
requiredArtifacts: requiredArtifacts,
|
|
190
|
+
optionalActions: structuralQualityOptionalActions(quality, state.taskId),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { assertJsonLimits } from "./json-safety.js";
|
|
2
|
+
import { assertSecretFree } from "./receipt.js";
|
|
3
|
+
import { E_PORTABLE_CONTEXT_INVALID } from "./error-codes.js";
|
|
4
|
+
|
|
5
|
+
export class PortableContextError extends Error {
|
|
6
|
+
constructor(message, { code = E_PORTABLE_CONTEXT_INVALID, cause } = {}) {
|
|
7
|
+
super(message, cause !== undefined ? { cause } : undefined);
|
|
8
|
+
this.name = "PortableContextError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PORTABLE_SECRET_PATTERNS = [
|
|
14
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
15
|
+
/(?:^|\s)(?:sk|ghp|glpat|xox[baprs])-[-_a-z0-9]{8,}/i,
|
|
16
|
+
/(?:AKIA|ASIA)[A-Z0-9]{12,}/,
|
|
17
|
+
/(?:bearer\s+[-_a-z0-9\.]{4,})/i,
|
|
18
|
+
/(?:authorization:\s*bearer)/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function containsSecretPattern(str) {
|
|
22
|
+
return PORTABLE_SECRET_PATTERNS.some((pattern) => pattern.test(str));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkObjectForSecrets(value, location = "$") {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
if (containsSecretPattern(value)) {
|
|
28
|
+
throw new Error(`${location}: secret-like value is not allowed`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!value || typeof value !== "object") return;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
value.forEach((item, index) => checkObjectForSecrets(item, `${location}[${index}]`));
|
|
35
|
+
} else {
|
|
36
|
+
for (const [key, child] of Object.entries(value)) {
|
|
37
|
+
checkObjectForSecrets(child, `${location}.${key}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizePortableText(
|
|
43
|
+
value,
|
|
44
|
+
{
|
|
45
|
+
label = "portable text",
|
|
46
|
+
maxLength,
|
|
47
|
+
optional = false,
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
if (value === undefined || value === null) {
|
|
51
|
+
if (optional) return null;
|
|
52
|
+
throw new PortableContextError(`${label} is required`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
56
|
+
throw new PortableContextError(`${label} must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
60
|
+
throw new PortableContextError(`${label} exceeds the ${maxLength}-character limit`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (/\p{Cc}/u.test(value)) {
|
|
64
|
+
throw new PortableContextError(`${label} contains control characters`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function assertPortableContextSafe(
|
|
71
|
+
value,
|
|
72
|
+
{
|
|
73
|
+
label = "portable context",
|
|
74
|
+
} = {},
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
assertJsonLimits(value, label);
|
|
78
|
+
assertSecretFree(value);
|
|
79
|
+
checkObjectForSecrets(value, label);
|
|
80
|
+
return value;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === E_PORTABLE_CONTEXT_INVALID) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
throw new PortableContextError(
|
|
86
|
+
`${label} failed safety verification: ${error.message}`,
|
|
87
|
+
{ code: E_PORTABLE_CONTEXT_INVALID, cause: error },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deepFreeze(object) {
|
|
93
|
+
if (object === null || typeof object !== "object" || Object.isFrozen(object)) {
|
|
94
|
+
return object;
|
|
95
|
+
}
|
|
96
|
+
Object.freeze(object);
|
|
97
|
+
for (const value of Object.values(object)) {
|
|
98
|
+
if (value !== null && typeof value === "object") {
|
|
99
|
+
deepFreeze(value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return object;
|
|
103
|
+
}
|