open-multi-agent-kit 0.90.8 → 0.91.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/CHANGELOG.md +41 -0
- package/README.md +21 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +140 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1179 -95
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -8
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +7 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/metrics-sampler.d.ts +20 -0
- package/dist/core/metrics-sampler.d.ts.map +1 -1
- package/dist/core/metrics-sampler.js +52 -0
- package/dist/core/metrics-sampler.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +16 -7
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +51 -0
- package/dist/core/provider-resilience.d.ts.map +1 -0
- package/dist/core/provider-resilience.js +86 -0
- package/dist/core/provider-resilience.js.map +1 -0
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1045 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +106 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +445 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +52 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +54 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/omp-pure-seams.d.ts +41 -0
- package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
- package/dist/core/tools/omp-pure-seams.js +142 -0
- package/dist/core/tools/omp-pure-seams.js.map +1 -0
- package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
- package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
- package/dist/core/tools/omp-seam-runtime.js +40 -0
- package/dist/core/tools/omp-seam-runtime.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +34 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -8
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
- package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/adaptorch-preview.md +1 -1
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/provider-resilience.md +87 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +80 -2
- package/docs/themes.md +2 -0
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +22 -13
- package/package.json +6 -5
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, browser-safe session doctor planner (task I38).
|
|
3
|
+
*
|
|
4
|
+
* The doctor rederives every repair decision from caller-attested,
|
|
5
|
+
* accessor-free plain data plus four pure runtime dependencies:
|
|
6
|
+
* - {@link createSessionRepairPlan} (session transcript repair),
|
|
7
|
+
* - {@link inspectRunJournal} / {@link serializeRunJournalLine} (journal
|
|
8
|
+
* integrity and terminal-record serialization),
|
|
9
|
+
* - {@link decideSessionPathAccess} (per-artifact path authorization),
|
|
10
|
+
* - {@link decideCompactionCommit} (compaction commit authorization).
|
|
11
|
+
*
|
|
12
|
+
* Runtime purity: no I/O, no wall clock, no entropy, no cryptographic
|
|
13
|
+
* primitives, and no Node-only APIs. Every journal hash is recomputed by a
|
|
14
|
+
* caller-injected synchronous hash function; the doctor never mints a
|
|
15
|
+
* termination or a hash. {@link SessionIntegrityReport} is imported as a type
|
|
16
|
+
* only, so the session-integrity module is never on this planner's runtime
|
|
17
|
+
* dependency graph. All inputs are validated strictly (plain data, exact keys,
|
|
18
|
+
* accessor-free) and the returned plan is a deep-frozen copy.
|
|
19
|
+
*/
|
|
20
|
+
import { decideCompactionCommit } from "./compaction/transaction.js";
|
|
21
|
+
import { GENESIS_HASH, inspectRunJournal, serializeRunJournalLine } from "./run-journal.js";
|
|
22
|
+
import { decideSessionPathAccess } from "./session-path-policy.js";
|
|
23
|
+
import { createSessionRepairPlan } from "./session-repair-plan.js";
|
|
24
|
+
export const SESSION_DOCTOR_PLAN_SCHEMA_VERSION = 1;
|
|
25
|
+
const MAX_IDENTIFIER_LENGTH = 128;
|
|
26
|
+
const MAX_NORMALIZED_CHECKS = 128;
|
|
27
|
+
const MAX_FINDING_IDS = 64;
|
|
28
|
+
const MAX_PLAIN_DATA_DEPTH = 64;
|
|
29
|
+
const MAX_PLAIN_DATA_NODES = 200_000;
|
|
30
|
+
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
|
|
31
|
+
/** High-confidence credential shapes; rejected at every trust boundary. */
|
|
32
|
+
const CREDENTIAL_SHAPES = [
|
|
33
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
34
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/,
|
|
35
|
+
/\bgh[pousr]_[A-Za-z0-9]{16,}\b/,
|
|
36
|
+
/\bgithub_pat_[A-Za-z0-9_]{16,}\b/,
|
|
37
|
+
/\bxox[baprs]-[A-Za-z0-9-]{16,}\b/,
|
|
38
|
+
/\bAKIA[A-Z0-9]{16}\b/,
|
|
39
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
|
|
40
|
+
/\bBearer\s+[A-Za-z0-9._~+/-]{8,}={0,2}\b/i,
|
|
41
|
+
/(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]+[_-])*(?:authorization|api[-_ ]?key|access[-_ ]?token|token|password|secret(?:[-_ ]?key)?|private[-_ ]?key|cookie)["']?\s*[:=]/i,
|
|
42
|
+
];
|
|
43
|
+
// -------------------------------------------------------------------------------------------------
|
|
44
|
+
// Generic plain-data / exact-key / accessor-free guards (fail closed).
|
|
45
|
+
// -------------------------------------------------------------------------------------------------
|
|
46
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
47
|
+
function isRecord(value) {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function isPlainDataObject(value) {
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
52
|
+
return false;
|
|
53
|
+
if (Object.getPrototypeOf(value) !== null && Object.getPrototypeOf(value) !== Object.prototype)
|
|
54
|
+
return false;
|
|
55
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
56
|
+
return false;
|
|
57
|
+
for (const key of Object.keys(value)) {
|
|
58
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
59
|
+
if (descriptor === undefined ||
|
|
60
|
+
descriptor.get !== undefined ||
|
|
61
|
+
descriptor.set !== undefined ||
|
|
62
|
+
!("value" in descriptor)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strictly validate a plain dense array before any indexed value access: it
|
|
70
|
+
* must be a real Array whose prototype is exactly Array.prototype, carry no
|
|
71
|
+
* symbol properties or extra own properties, and expose every index 0..length-1
|
|
72
|
+
* as an own data descriptor (no holes, no getters/setters). Accessor indices are
|
|
73
|
+
* rejected without their getters ever executing. The length is captured once.
|
|
74
|
+
*/
|
|
75
|
+
function assertStrictPlainArray(value, label) {
|
|
76
|
+
if (typeof value !== "object" || value === null || !Array.isArray(value)) {
|
|
77
|
+
throw new TypeError(`${label} must be an array`);
|
|
78
|
+
}
|
|
79
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
80
|
+
throw new TypeError(`${label} must use the Array prototype`);
|
|
81
|
+
}
|
|
82
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
83
|
+
throw new TypeError(`${label} must not carry symbol properties`);
|
|
84
|
+
}
|
|
85
|
+
const length = value.length;
|
|
86
|
+
for (const name of Object.getOwnPropertyNames(value)) {
|
|
87
|
+
if (name === "length")
|
|
88
|
+
continue;
|
|
89
|
+
const index = Number(name);
|
|
90
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== name) {
|
|
91
|
+
throw new TypeError(`${label} must not carry extra own properties`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (let i = 0; i < length; i += 1) {
|
|
95
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
96
|
+
if (descriptor === undefined)
|
|
97
|
+
throw new TypeError(`${label} must be dense (no holes)`);
|
|
98
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined || !("value" in descriptor)) {
|
|
99
|
+
throw new TypeError(`${label} must not expose accessor or non-data element descriptors`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function looksLikeCredential(value) {
|
|
104
|
+
return CREDENTIAL_SHAPES.some((pattern) => pattern.test(value));
|
|
105
|
+
}
|
|
106
|
+
function assertPlainData(value, label) {
|
|
107
|
+
if (!isPlainDataObject(value)) {
|
|
108
|
+
throw new TypeError(`${label} must be a plain data object without accessors or a foreign prototype`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Bounded recursive plain-data validator for trust boundaries. Walks the full
|
|
113
|
+
* value tree (plain objects and dense arrays) and rejects any accessor
|
|
114
|
+
* property, symbol property, foreign prototype, or sparse array slot without
|
|
115
|
+
* ever executing a getter (descriptors are inspected, never read). Guards
|
|
116
|
+
* SessionIntegrityReport / journal terminalRecord / compaction before any of
|
|
117
|
+
* their fields are read. Primitives pass; Uint8Array and functions are not
|
|
118
|
+
* validated here (callers handle those explicitly).
|
|
119
|
+
*/
|
|
120
|
+
function assertPlainDataTree(value, label) {
|
|
121
|
+
let nodes = 0;
|
|
122
|
+
const stack = [{ value, label, depth: 0 }];
|
|
123
|
+
while (stack.length > 0) {
|
|
124
|
+
const frame = stack.pop();
|
|
125
|
+
nodes += 1;
|
|
126
|
+
if (nodes > MAX_PLAIN_DATA_NODES) {
|
|
127
|
+
throw new TypeError(`${label}: plain-data tree exceeds the maximum node budget`);
|
|
128
|
+
}
|
|
129
|
+
const { value: current, label: currentLabel, depth } = frame;
|
|
130
|
+
if (depth > MAX_PLAIN_DATA_DEPTH) {
|
|
131
|
+
throw new TypeError(`${currentLabel}: plain-data tree exceeds the maximum depth`);
|
|
132
|
+
}
|
|
133
|
+
if (current === null || typeof current !== "object")
|
|
134
|
+
continue;
|
|
135
|
+
if (Array.isArray(current)) {
|
|
136
|
+
assertStrictPlainArray(current, currentLabel);
|
|
137
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
138
|
+
stack.push({ value: current[index], label: `${currentLabel}[${index}]`, depth: depth + 1 });
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const prototype = Object.getPrototypeOf(current);
|
|
143
|
+
if (prototype !== null && prototype !== Object.prototype) {
|
|
144
|
+
throw new TypeError(`${currentLabel} must be a plain data object without a foreign prototype`);
|
|
145
|
+
}
|
|
146
|
+
if (Object.getOwnPropertySymbols(current).length > 0) {
|
|
147
|
+
throw new TypeError(`${currentLabel} must be a plain data object without symbol properties`);
|
|
148
|
+
}
|
|
149
|
+
for (const key of Object.keys(current)) {
|
|
150
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
151
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
152
|
+
throw new TypeError(`${currentLabel}.${key} must be a plain data property without accessors`);
|
|
153
|
+
}
|
|
154
|
+
stack.push({ value: descriptor.value, label: `${currentLabel}.${key}`, depth: depth + 1 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function assertIdentifier(value, label) {
|
|
159
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
160
|
+
throw new TypeError(`${label} must be 1-${MAX_IDENTIFIER_LENGTH} characters of bounded text`);
|
|
161
|
+
}
|
|
162
|
+
if (CONTROL_CHAR_RE.test(value)) {
|
|
163
|
+
throw new TypeError(`${label} must not contain C0 or DEL control characters`);
|
|
164
|
+
}
|
|
165
|
+
if (looksLikeCredential(value)) {
|
|
166
|
+
throw new TypeError(`${label} must not contain a credential-shaped literal`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function assertSafeNonNegativeInt(value, label) {
|
|
170
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
171
|
+
throw new TypeError(`${label} must be a non-negative safe integer`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
175
|
+
for (const key of Object.keys(value)) {
|
|
176
|
+
if (!allowed.includes(key)) {
|
|
177
|
+
throw new TypeError(`${label} contains an unsupported field: ${key}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// -------------------------------------------------------------------------------------------------
|
|
182
|
+
// Input validation.
|
|
183
|
+
// -------------------------------------------------------------------------------------------------
|
|
184
|
+
const INPUT_ALLOWED_KEYS = [
|
|
185
|
+
"schemaVersion",
|
|
186
|
+
"mode",
|
|
187
|
+
"sessionId",
|
|
188
|
+
"repairId",
|
|
189
|
+
"timestamp",
|
|
190
|
+
"report",
|
|
191
|
+
"journal",
|
|
192
|
+
"paths",
|
|
193
|
+
"compaction",
|
|
194
|
+
"normalizedChecks",
|
|
195
|
+
];
|
|
196
|
+
const MODES = new Set(["inspect", "repair_dry_run", "repair"]);
|
|
197
|
+
const JOURNAL_KEYS = ["bytes", "hashFn", "terminalRecord"];
|
|
198
|
+
const PATHS_KEYS = ["session", "journal"];
|
|
199
|
+
const PATH_INPUT_KEYS = ["platform", "root", "target", "identity", "evidence", "lock"];
|
|
200
|
+
const COMPACTION_KEYS = ["transaction", "currentRevision", "currentSource", "barrier", "priorCommittedSourceDigests"];
|
|
201
|
+
const NORMALIZED_KEYS = ["artifact", "id", "status"];
|
|
202
|
+
const NORMALIZED_ARTIFACTS = new Set([
|
|
203
|
+
"compaction_envelope",
|
|
204
|
+
"evidence_link",
|
|
205
|
+
"workspace",
|
|
206
|
+
"provider_model",
|
|
207
|
+
]);
|
|
208
|
+
const NORMALIZED_STATUSES = new Set(["ok", "missing", "invalid", "unavailable"]);
|
|
209
|
+
function validateNormalizedChecks(value) {
|
|
210
|
+
assertStrictPlainArray(value, "normalizedChecks");
|
|
211
|
+
if (value.length > MAX_NORMALIZED_CHECKS) {
|
|
212
|
+
throw new TypeError(`normalizedChecks must contain at most ${MAX_NORMALIZED_CHECKS} entries`);
|
|
213
|
+
}
|
|
214
|
+
const checks = [];
|
|
215
|
+
for (let index = 0; index < value.length; index++) {
|
|
216
|
+
const entry = value[index];
|
|
217
|
+
assertPlainData(entry, `normalizedChecks[${index}]`);
|
|
218
|
+
assertOnlyKeys(entry, NORMALIZED_KEYS, `normalizedChecks[${index}]`);
|
|
219
|
+
if (!NORMALIZED_ARTIFACTS.has(entry.artifact)) {
|
|
220
|
+
throw new TypeError(`normalizedChecks[${index}].artifact is out of bounds`);
|
|
221
|
+
}
|
|
222
|
+
assertIdentifier(entry.id, `normalizedChecks[${index}].id`);
|
|
223
|
+
if (!NORMALIZED_STATUSES.has(entry.status)) {
|
|
224
|
+
throw new TypeError(`normalizedChecks[${index}].status is out of bounds`);
|
|
225
|
+
}
|
|
226
|
+
checks.push({
|
|
227
|
+
artifact: entry.artifact,
|
|
228
|
+
id: entry.id,
|
|
229
|
+
status: entry.status,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return Object.freeze(checks);
|
|
233
|
+
}
|
|
234
|
+
function validatePathSubject(value, label) {
|
|
235
|
+
assertPlainData(value, label);
|
|
236
|
+
assertOnlyKeys(value, PATH_INPUT_KEYS, label);
|
|
237
|
+
if (typeof value.platform !== "string" || (value.platform !== "posix" && value.platform !== "win32")) {
|
|
238
|
+
throw new TypeError(`${label}.platform must be posix|win32`);
|
|
239
|
+
}
|
|
240
|
+
if (typeof value.root !== "string")
|
|
241
|
+
throw new TypeError(`${label}.root must be a string`);
|
|
242
|
+
if (typeof value.target !== "string")
|
|
243
|
+
throw new TypeError(`${label}.target must be a string`);
|
|
244
|
+
if (!Object.hasOwn(value, "evidence"))
|
|
245
|
+
throw new TypeError(`${label}.evidence is required`);
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
function validateInput(rawInput) {
|
|
249
|
+
assertPlainData(rawInput, "input");
|
|
250
|
+
assertOnlyKeys(rawInput, INPUT_ALLOWED_KEYS, "input");
|
|
251
|
+
if (rawInput.schemaVersion !== undefined && rawInput.schemaVersion !== SESSION_DOCTOR_PLAN_SCHEMA_VERSION) {
|
|
252
|
+
throw new TypeError("unsupported session doctor plan schema version");
|
|
253
|
+
}
|
|
254
|
+
if (!MODES.has(rawInput.mode)) {
|
|
255
|
+
throw new TypeError("mode must be inspect|repair_dry_run|repair");
|
|
256
|
+
}
|
|
257
|
+
assertIdentifier(rawInput.sessionId, "sessionId");
|
|
258
|
+
assertIdentifier(rawInput.repairId, "repairId");
|
|
259
|
+
assertSafeNonNegativeInt(rawInput.timestamp, "timestamp");
|
|
260
|
+
assertPlainData(rawInput.report, "report");
|
|
261
|
+
assertPlainDataTree(rawInput.report, "report");
|
|
262
|
+
let journal = null;
|
|
263
|
+
if (Object.hasOwn(rawInput, "journal") && rawInput.journal !== undefined) {
|
|
264
|
+
assertPlainData(rawInput.journal, "journal");
|
|
265
|
+
assertOnlyKeys(rawInput.journal, JOURNAL_KEYS, "journal");
|
|
266
|
+
const sourceBytes = rawInput.journal.bytes;
|
|
267
|
+
if (!(sourceBytes instanceof Uint8Array)) {
|
|
268
|
+
throw new TypeError("journal.bytes must be a Uint8Array");
|
|
269
|
+
}
|
|
270
|
+
// Defensive copy: isolate the plan from caller buffer aliasing and from
|
|
271
|
+
// hashFn closures that mutate the caller's bytes. The plan inspects and
|
|
272
|
+
// re-inspects this stable private copy.
|
|
273
|
+
const bytes = new Uint8Array(sourceBytes);
|
|
274
|
+
if (typeof rawInput.journal.hashFn !== "function") {
|
|
275
|
+
throw new TypeError("journal.hashFn must be a function");
|
|
276
|
+
}
|
|
277
|
+
let terminalRecord;
|
|
278
|
+
if (Object.hasOwn(rawInput.journal, "terminalRecord") && rawInput.journal.terminalRecord !== undefined) {
|
|
279
|
+
assertPlainData(rawInput.journal.terminalRecord, "journal.terminalRecord");
|
|
280
|
+
assertPlainDataTree(rawInput.journal.terminalRecord, "journal.terminalRecord");
|
|
281
|
+
terminalRecord = rawInput.journal.terminalRecord;
|
|
282
|
+
}
|
|
283
|
+
journal = Object.freeze({
|
|
284
|
+
bytes,
|
|
285
|
+
hashFn: rawInput.journal.hashFn,
|
|
286
|
+
...(terminalRecord === undefined ? {} : { terminalRecord }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
let pathsSession = null;
|
|
290
|
+
let pathsJournal = null;
|
|
291
|
+
if (Object.hasOwn(rawInput, "paths") && rawInput.paths !== undefined) {
|
|
292
|
+
assertPlainData(rawInput.paths, "paths");
|
|
293
|
+
assertOnlyKeys(rawInput.paths, PATHS_KEYS, "paths");
|
|
294
|
+
if (Object.hasOwn(rawInput.paths, "session") && rawInput.paths.session !== undefined) {
|
|
295
|
+
pathsSession = validatePathSubject(rawInput.paths.session, "paths.session");
|
|
296
|
+
}
|
|
297
|
+
if (Object.hasOwn(rawInput.paths, "journal") && rawInput.paths.journal !== undefined) {
|
|
298
|
+
pathsJournal = validatePathSubject(rawInput.paths.journal, "paths.journal");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let compaction = null;
|
|
302
|
+
if (Object.hasOwn(rawInput, "compaction") && rawInput.compaction !== undefined) {
|
|
303
|
+
assertPlainData(rawInput.compaction, "compaction");
|
|
304
|
+
assertOnlyKeys(rawInput.compaction, COMPACTION_KEYS, "compaction");
|
|
305
|
+
assertPlainDataTree(rawInput.compaction, "compaction");
|
|
306
|
+
compaction = rawInput.compaction;
|
|
307
|
+
}
|
|
308
|
+
const normalizedChecks = Object.hasOwn(rawInput, "normalizedChecks") && rawInput.normalizedChecks !== undefined
|
|
309
|
+
? validateNormalizedChecks(rawInput.normalizedChecks)
|
|
310
|
+
: Object.freeze([]);
|
|
311
|
+
return {
|
|
312
|
+
mode: rawInput.mode,
|
|
313
|
+
sessionId: rawInput.sessionId,
|
|
314
|
+
repairId: rawInput.repairId,
|
|
315
|
+
timestamp: rawInput.timestamp,
|
|
316
|
+
report: rawInput.report,
|
|
317
|
+
journal,
|
|
318
|
+
paths: Object.freeze({ session: pathsSession, journal: pathsJournal }),
|
|
319
|
+
compaction,
|
|
320
|
+
normalizedChecks,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// -------------------------------------------------------------------------------------------------
|
|
324
|
+
// Pure helpers.
|
|
325
|
+
// -------------------------------------------------------------------------------------------------
|
|
326
|
+
function intentForMode(mode) {
|
|
327
|
+
if (mode === "inspect")
|
|
328
|
+
return "inspect_content";
|
|
329
|
+
if (mode === "repair_dry_run")
|
|
330
|
+
return "repair_dry_run";
|
|
331
|
+
return "repair";
|
|
332
|
+
}
|
|
333
|
+
function finding(artifact, reason, severity, code, ids) {
|
|
334
|
+
const result = { artifact, reason, severity };
|
|
335
|
+
if (code !== undefined && code.length > 0) {
|
|
336
|
+
if (code.length > MAX_IDENTIFIER_LENGTH) {
|
|
337
|
+
throw new TypeError("finding code exceeds the bounded length");
|
|
338
|
+
}
|
|
339
|
+
result.code = code;
|
|
340
|
+
}
|
|
341
|
+
if (ids !== undefined && ids.length > 0) {
|
|
342
|
+
if (ids.length > MAX_FINDING_IDS) {
|
|
343
|
+
throw new TypeError(`finding ids exceed the bounded limit of ${MAX_FINDING_IDS}; failing closed`);
|
|
344
|
+
}
|
|
345
|
+
for (const id of ids)
|
|
346
|
+
assertIdentifier(id, "finding id");
|
|
347
|
+
result.ids = Object.freeze([...ids]);
|
|
348
|
+
}
|
|
349
|
+
return Object.freeze(result);
|
|
350
|
+
}
|
|
351
|
+
function deepFreeze(value) {
|
|
352
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
353
|
+
return value;
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
for (const item of value)
|
|
356
|
+
deepFreeze(item);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
for (const child of Object.values(value))
|
|
360
|
+
deepFreeze(child);
|
|
361
|
+
}
|
|
362
|
+
return Object.freeze(value);
|
|
363
|
+
}
|
|
364
|
+
function deepCopy(value) {
|
|
365
|
+
if (typeof value !== "object" || value === null)
|
|
366
|
+
return value;
|
|
367
|
+
if (Array.isArray(value))
|
|
368
|
+
return deepFreeze(value.map(deepCopy));
|
|
369
|
+
const out = {};
|
|
370
|
+
for (const key of Object.keys(value)) {
|
|
371
|
+
out[key] = deepCopy(value[key]);
|
|
372
|
+
}
|
|
373
|
+
return deepFreeze(out);
|
|
374
|
+
}
|
|
375
|
+
function appendRecoveryRecord(bytes, completePrefix, record) {
|
|
376
|
+
const suffix = TEXT_ENCODER.encode(`${serializeRunJournalLine(record)}\n`);
|
|
377
|
+
const out = new Uint8Array(completePrefix.byteCount + suffix.byteLength);
|
|
378
|
+
out.set(bytes.subarray(0, completePrefix.byteCount));
|
|
379
|
+
out.set(suffix, completePrefix.byteCount);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function dedupPreserveOrder(values) {
|
|
383
|
+
const seen = new Set();
|
|
384
|
+
const out = [];
|
|
385
|
+
for (const value of values) {
|
|
386
|
+
if (!seen.has(value)) {
|
|
387
|
+
seen.add(value);
|
|
388
|
+
out.push(value);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
const STALE_LOCK_ARTIFACT_RANK = {
|
|
394
|
+
session: 0,
|
|
395
|
+
journal: 1,
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Canonical physical target key for stale-lock dedup. Win32 compares
|
|
399
|
+
* case-insensitively (lower-cased); POSIX is exact. The platform is intentionally
|
|
400
|
+
* not part of the key, so a target described under conflicting platforms is
|
|
401
|
+
* surfaced as a contract violation rather than silently duplicated.
|
|
402
|
+
*/
|
|
403
|
+
function canonicalStaleLockTargetKey(platform, targetLexical, targetRealpath) {
|
|
404
|
+
const lexical = platform === "win32" ? targetLexical.toLowerCase() : targetLexical;
|
|
405
|
+
const realpath = platform === "win32" ? targetRealpath.toLowerCase() : targetRealpath;
|
|
406
|
+
return JSON.stringify([lexical, realpath]);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Deduplicate stale-lock actions from the session and journal paths by their
|
|
410
|
+
* canonical physical target, merging the contributing artifacts deterministically
|
|
411
|
+
* (sorted by artifact rank). A target described under conflicting platforms or
|
|
412
|
+
* conflicting holder pids is a contract violation: the plan is refused and no
|
|
413
|
+
* actions are reported for that target.
|
|
414
|
+
*/
|
|
415
|
+
function dedupStaleLockActions(specs, acc) {
|
|
416
|
+
if (specs.length <= 1)
|
|
417
|
+
return [...specs];
|
|
418
|
+
const groups = new Map();
|
|
419
|
+
for (const spec of specs) {
|
|
420
|
+
const key = canonicalStaleLockTargetKey(spec.platform, spec.targetLexical, spec.targetRealpath);
|
|
421
|
+
const group = groups.get(key);
|
|
422
|
+
if (group === undefined) {
|
|
423
|
+
groups.set(key, [spec]);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
group.push(spec);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const out = [];
|
|
430
|
+
for (const group of groups.values()) {
|
|
431
|
+
if (group.length === 1) {
|
|
432
|
+
out.push(group[0]);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const platforms = new Set(group.map((spec) => spec.platform));
|
|
436
|
+
const holders = new Set(group.map((spec) => spec.holderPid));
|
|
437
|
+
if (platforms.size > 1 || holders.size > 1) {
|
|
438
|
+
acc.findings.push(finding("path", "contract_violation", "blocked", "stale_lock_target_conflict"));
|
|
439
|
+
acc.refused = true;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const mergedArtifacts = Array.from(new Set(group.flatMap((spec) => spec.artifacts))).sort((a, b) => STALE_LOCK_ARTIFACT_RANK[a] - STALE_LOCK_ARTIFACT_RANK[b]);
|
|
443
|
+
out.push({ ...group[0], artifacts: Object.freeze(mergedArtifacts) });
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
// Finding sort: stable total ordering by artifact, reason, code, then ids.
|
|
448
|
+
const ARTIFACT_RANK = {
|
|
449
|
+
session: 0,
|
|
450
|
+
journal: 1,
|
|
451
|
+
path: 2,
|
|
452
|
+
compaction: 3,
|
|
453
|
+
normalized: 4,
|
|
454
|
+
};
|
|
455
|
+
function findingSortKey(value) {
|
|
456
|
+
return `${ARTIFACT_RANK[value.artifact]}|${value.reason}|${value.code ?? ""}|${(value.ids ?? []).join(",")}`;
|
|
457
|
+
}
|
|
458
|
+
function sortFindings(findings) {
|
|
459
|
+
return [...findings].sort((a, b) => {
|
|
460
|
+
const left = findingSortKey(a);
|
|
461
|
+
const right = findingSortKey(b);
|
|
462
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const REPAIRABLE_SESSION_BLOCKER_REASONS = new Set([
|
|
466
|
+
"integrity_finding",
|
|
467
|
+
"inspection_incomplete",
|
|
468
|
+
"ambiguous_transcript",
|
|
469
|
+
"inconsistent_report",
|
|
470
|
+
]);
|
|
471
|
+
function deriveSession(input, acc) {
|
|
472
|
+
const report = input.report;
|
|
473
|
+
// Cross-bind the session header identity to the doctored session id.
|
|
474
|
+
const header = report.header;
|
|
475
|
+
if (header !== null && isRecord(header) && typeof header.id === "string" && header.id !== input.sessionId) {
|
|
476
|
+
acc.findings.push(finding("session", "contract_violation", "blocked", "session_identity_mismatch"));
|
|
477
|
+
acc.refused = true;
|
|
478
|
+
}
|
|
479
|
+
// Ignore report.ok: createSessionRepairPlan rederives consistency and the
|
|
480
|
+
// repairable action set with a fixed doctor_repair reason.
|
|
481
|
+
const sessionPlan = createSessionRepairPlan(report, {
|
|
482
|
+
repairId: input.repairId,
|
|
483
|
+
reason: "doctor_repair",
|
|
484
|
+
timestamp: input.timestamp,
|
|
485
|
+
});
|
|
486
|
+
if (sessionPlan.status === "repairable") {
|
|
487
|
+
let quarantined = false;
|
|
488
|
+
const missingIds = [];
|
|
489
|
+
const appendSpecs = [];
|
|
490
|
+
for (const action of sessionPlan.actions) {
|
|
491
|
+
if (action.kind === "quarantine_trailing_fragment") {
|
|
492
|
+
quarantined = true;
|
|
493
|
+
acc.sessionSpecs.push({
|
|
494
|
+
kind: "quarantine_session_trailing_fragment",
|
|
495
|
+
artifact: "session",
|
|
496
|
+
retainPrefix: { ...action.retainPrefix },
|
|
497
|
+
fragment: { ...action.fragment },
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else if (action.kind === "append_synthetic_tool_result") {
|
|
501
|
+
missingIds.push(action.toolCallId);
|
|
502
|
+
// Prevalidate the synthetic message as plain data before deep-copying
|
|
503
|
+
// it into the action, so a non-plain runtime payload fails closed.
|
|
504
|
+
assertPlainDataTree(action.message, "session synthetic tool result message");
|
|
505
|
+
appendSpecs.push({
|
|
506
|
+
kind: "append_synthetic_tool_result",
|
|
507
|
+
artifact: "session",
|
|
508
|
+
toolCallId: action.toolCallId,
|
|
509
|
+
toolName: action.toolName,
|
|
510
|
+
repairSequence: action.sequence,
|
|
511
|
+
message: deepCopy(action.message),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Normalize session action order regardless of upstream: quarantine first,
|
|
516
|
+
// then synthetic appends sorted by their upstream repair sequence.
|
|
517
|
+
appendSpecs.sort((a, b) => a.repairSequence - b.repairSequence);
|
|
518
|
+
for (const spec of appendSpecs)
|
|
519
|
+
acc.sessionSpecs.push(spec);
|
|
520
|
+
if (quarantined) {
|
|
521
|
+
acc.findings.push(finding("session", "session_trailing_fragment", "blocked"));
|
|
522
|
+
}
|
|
523
|
+
if (missingIds.length > 0) {
|
|
524
|
+
acc.findings.push(finding("session", "session_missing_tool_result", "blocked", undefined, missingIds));
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (sessionPlan.status === "blocked") {
|
|
529
|
+
for (const blocker of sessionPlan.blockers) {
|
|
530
|
+
if (!REPAIRABLE_SESSION_BLOCKER_REASONS.has(blocker.reason)) {
|
|
531
|
+
acc.findings.push(finding("session", "contract_violation", "blocked", "unknown_blocker"));
|
|
532
|
+
acc.refused = true;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
switch (blocker.reason) {
|
|
536
|
+
case "inconsistent_report":
|
|
537
|
+
acc.findings.push(finding("session", "contract_violation", "blocked", "inconsistent_report"));
|
|
538
|
+
acc.refused = true;
|
|
539
|
+
break;
|
|
540
|
+
case "inspection_incomplete":
|
|
541
|
+
acc.findings.push(finding("session", "session_inspection_incomplete", "blocked"));
|
|
542
|
+
break;
|
|
543
|
+
case "ambiguous_transcript":
|
|
544
|
+
acc.findings.push(finding("session", "session_blocked", "blocked", "ambiguous_transcript"));
|
|
545
|
+
break;
|
|
546
|
+
case "integrity_finding": {
|
|
547
|
+
const ids = [];
|
|
548
|
+
if (blocker.entryId !== undefined)
|
|
549
|
+
ids.push(blocker.entryId);
|
|
550
|
+
if (blocker.toolCallId !== undefined)
|
|
551
|
+
ids.push(blocker.toolCallId);
|
|
552
|
+
acc.findings.push(finding("session", "session_blocked", "blocked", blocker.findingReason, ids.length > 0 ? ids : undefined));
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
default:
|
|
556
|
+
acc.findings.push(finding("session", "contract_violation", "blocked", "unknown_blocker"));
|
|
557
|
+
acc.refused = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// status "not_needed": nothing to observe for the session artifact.
|
|
563
|
+
}
|
|
564
|
+
function deriveJournal(input, acc) {
|
|
565
|
+
const observation = input.journal;
|
|
566
|
+
if (observation === null)
|
|
567
|
+
return;
|
|
568
|
+
const report = inspectRunJournal(observation.bytes, observation.hashFn);
|
|
569
|
+
const records = report.records;
|
|
570
|
+
const findingCodes = report.findings.map((f) => f.code);
|
|
571
|
+
const codeSet = new Set(findingCodes);
|
|
572
|
+
const hasSessionMismatch = codeSet.has("session_mismatch");
|
|
573
|
+
const allBound = records.length === 0 || records.every((record) => record.sessionId === input.sessionId);
|
|
574
|
+
// Cross-bind every journal record session id to the doctored session id.
|
|
575
|
+
if (!allBound || hasSessionMismatch) {
|
|
576
|
+
acc.findings.push(finding("journal", "contract_violation", "blocked", "session_identity_mismatch"));
|
|
577
|
+
acc.refused = true;
|
|
578
|
+
}
|
|
579
|
+
const trailing = codeSet.has("trailing_fragment");
|
|
580
|
+
const unclosed = codeSet.has("run_unclosed");
|
|
581
|
+
const hardCodes = dedupPreserveOrder(findingCodes.filter((code) => code !== "trailing_fragment" && code !== "run_unclosed" && code !== "session_mismatch"));
|
|
582
|
+
const journalCanAct = hardCodes.length === 0 && allBound && !hasSessionMismatch;
|
|
583
|
+
for (const code of hardCodes) {
|
|
584
|
+
acc.findings.push(finding("journal", "journal_blocked", "blocked", code));
|
|
585
|
+
}
|
|
586
|
+
// Unclosed-run recovery: only when findings are a subset of {run_unclosed, trailing_fragment}.
|
|
587
|
+
const recoverableState = unclosed && journalCanAct;
|
|
588
|
+
let recoverySucceeded = false;
|
|
589
|
+
if (unclosed) {
|
|
590
|
+
const runIds = report.openRunId !== null ? [report.openRunId] : undefined;
|
|
591
|
+
acc.findings.push(finding("journal", "journal_unclosed", "blocked", undefined, runIds));
|
|
592
|
+
const supplied = Object.hasOwn(observation, "terminalRecord") ? observation.terminalRecord : undefined;
|
|
593
|
+
if (supplied !== undefined) {
|
|
594
|
+
if (!recoverableState) {
|
|
595
|
+
// Spurious: a recovery record was supplied for a journal that is not in the recoverable state.
|
|
596
|
+
acc.findings.push(finding("journal", "contract_violation", "blocked", "spurious_terminal_record"));
|
|
597
|
+
acc.refused = true;
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const recheck = inspectRunJournal(appendRecoveryRecord(observation.bytes, report.completePrefix, supplied), observation.hashFn);
|
|
601
|
+
const last = recheck.records.length > 0 ? recheck.records[recheck.records.length - 1] : null;
|
|
602
|
+
const identityOk = last !== null && serializeRunJournalLine(last) === serializeRunJournalLine(supplied);
|
|
603
|
+
if (recheck.ok &&
|
|
604
|
+
recheck.openRunId === null &&
|
|
605
|
+
last !== null &&
|
|
606
|
+
last.event === "run_recovered" &&
|
|
607
|
+
identityOk) {
|
|
608
|
+
recoverySucceeded = true;
|
|
609
|
+
const lastCompleteHash = records.length > 0 ? records[records.length - 1].hash : GENESIS_HASH;
|
|
610
|
+
acc.journalSpecs.push({
|
|
611
|
+
kind: "recover_run",
|
|
612
|
+
artifact: "journal",
|
|
613
|
+
runId: last.runId,
|
|
614
|
+
terminalRecord: deepCopy(supplied),
|
|
615
|
+
completePrefix: {
|
|
616
|
+
byteCount: report.completePrefix.byteCount,
|
|
617
|
+
lineCount: report.completePrefix.lineCount,
|
|
618
|
+
},
|
|
619
|
+
lastCompleteHash,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
acc.findings.push(finding("journal", "contract_violation", "blocked", "forged_terminal_record"));
|
|
624
|
+
acc.refused = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
const supplied = Object.hasOwn(observation, "terminalRecord") ? observation.terminalRecord : undefined;
|
|
631
|
+
if (supplied !== undefined) {
|
|
632
|
+
// Spurious: a recovery record was supplied with no unclosed run to recover.
|
|
633
|
+
acc.findings.push(finding("journal", "contract_violation", "blocked", "spurious_terminal_record"));
|
|
634
|
+
acc.refused = true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Trailing fragment: quarantine the journal tail, anchored on the complete
|
|
638
|
+
// prefix and the last complete hash (genesis if none). When recovery
|
|
639
|
+
// succeeded the recover_run action carries completePrefix + lastCompleteHash,
|
|
640
|
+
// explicitly anchoring a truncate-to-prefix-then-append that already discards
|
|
641
|
+
// the trailing fragment, so it subsumes the quarantine action.
|
|
642
|
+
if (trailing && journalCanAct && !recoverySucceeded) {
|
|
643
|
+
acc.findings.push(finding("journal", "journal_trailing_fragment", "blocked"));
|
|
644
|
+
const lastCompleteHash = records.length > 0 ? records[records.length - 1].hash : GENESIS_HASH;
|
|
645
|
+
acc.journalSpecs.push({
|
|
646
|
+
kind: "quarantine_journal_trailing_fragment",
|
|
647
|
+
artifact: "journal",
|
|
648
|
+
completePrefix: { byteCount: report.completePrefix.byteCount, lineCount: report.completePrefix.lineCount },
|
|
649
|
+
lastCompleteHash,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function deriveCompaction(input, acc) {
|
|
654
|
+
const observation = input.compaction;
|
|
655
|
+
if (observation === null)
|
|
656
|
+
return;
|
|
657
|
+
// Cross-bind the transaction base/source session id before rederiving.
|
|
658
|
+
const tx = observation.transaction;
|
|
659
|
+
const baseSessionId = isRecord(tx) && isRecord(tx.baseRevision) && typeof tx.baseRevision.sessionId === "string"
|
|
660
|
+
? tx.baseRevision.sessionId
|
|
661
|
+
: null;
|
|
662
|
+
const sourceSessionId = isRecord(tx) && isRecord(tx.source) && typeof tx.source.sessionId === "string"
|
|
663
|
+
? tx.source.sessionId
|
|
664
|
+
: null;
|
|
665
|
+
if (baseSessionId !== input.sessionId || sourceSessionId !== input.sessionId) {
|
|
666
|
+
acc.findings.push(finding("compaction", "contract_violation", "blocked", "session_identity_mismatch"));
|
|
667
|
+
acc.refused = true;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const decision = decideCompactionCommit(observation);
|
|
671
|
+
switch (decision.decision) {
|
|
672
|
+
case "stale":
|
|
673
|
+
acc.findings.push(finding("compaction", "compaction_stale", "blocked"));
|
|
674
|
+
acc.compactionSpecs.push({
|
|
675
|
+
kind: "abandon_stale_compaction",
|
|
676
|
+
artifact: "session",
|
|
677
|
+
transactionId: decision.transactionId,
|
|
678
|
+
baseRevision: deepCopy(tx.baseRevision),
|
|
679
|
+
sourceSha256: tx.source.sourceSha256,
|
|
680
|
+
staleReason: decision.reason,
|
|
681
|
+
});
|
|
682
|
+
break;
|
|
683
|
+
case "defer":
|
|
684
|
+
acc.findings.push(finding("compaction", "compaction_deferred", "blocked", decision.barrierReason));
|
|
685
|
+
break;
|
|
686
|
+
case "fail_closed":
|
|
687
|
+
if (decision.reason === "barrier_fail_closed") {
|
|
688
|
+
acc.findings.push(finding("compaction", "compaction_failed", "blocked", decision.barrierReason));
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
acc.findings.push(finding("compaction", "contract_violation", "blocked", decision.reason));
|
|
692
|
+
acc.refused = true;
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
case "commit":
|
|
696
|
+
acc.findings.push(finding("compaction", "compaction_committed", "advisory"));
|
|
697
|
+
break;
|
|
698
|
+
case "duplicate":
|
|
699
|
+
acc.findings.push(finding("compaction", "compaction_duplicate", "advisory"));
|
|
700
|
+
break;
|
|
701
|
+
default:
|
|
702
|
+
acc.findings.push(finding("compaction", "contract_violation", "blocked", "unknown_decision"));
|
|
703
|
+
acc.refused = true;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function evaluatePath(subject, intent) {
|
|
708
|
+
if (subject === null) {
|
|
709
|
+
return { decision: null, authorized: false, staleLock: null };
|
|
710
|
+
}
|
|
711
|
+
const decision = decideSessionPathAccess({ ...subject, intent });
|
|
712
|
+
if (decision.status === "authorized") {
|
|
713
|
+
const stale = decision.plannedActions.find((action) => action.kind === "remove_stale_lock");
|
|
714
|
+
if (stale !== undefined && stale.kind === "remove_stale_lock") {
|
|
715
|
+
return {
|
|
716
|
+
decision,
|
|
717
|
+
authorized: true,
|
|
718
|
+
staleLock: {
|
|
719
|
+
platform: decision.platform,
|
|
720
|
+
targetLexical: decision.targetLexical,
|
|
721
|
+
targetRealpath: decision.targetRealpath,
|
|
722
|
+
holderPid: stale.holderPid,
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return { decision, authorized: true, staleLock: null };
|
|
727
|
+
}
|
|
728
|
+
return { decision, authorized: false, staleLock: null };
|
|
729
|
+
}
|
|
730
|
+
function derivePaths(input, acc, sessionPathNeeded, journalPathNeeded) {
|
|
731
|
+
const intent = intentForMode(input.mode);
|
|
732
|
+
const requiresAuthorized = input.mode === "repair" || input.mode === "repair_dry_run";
|
|
733
|
+
const sessionAuth = evaluatePath(input.paths.session, intent);
|
|
734
|
+
authorizeArtifact(acc, sessionAuth, sessionPathNeeded, "session", "session_path_unauthorized", requiresAuthorized);
|
|
735
|
+
const journalAuth = evaluatePath(input.paths.journal, intent);
|
|
736
|
+
authorizeArtifact(acc, journalAuth, journalPathNeeded, "journal", "journal_path_unauthorized", requiresAuthorized);
|
|
737
|
+
}
|
|
738
|
+
function authorizeArtifact(acc, auth, needed, artifact, reason, requiresAuthorized) {
|
|
739
|
+
if (auth.authorized) {
|
|
740
|
+
// Eligible stale-lock cleanup from this path decision only.
|
|
741
|
+
if (auth.staleLock !== null) {
|
|
742
|
+
acc.pathSpecs.push({
|
|
743
|
+
kind: "remove_stale_lock",
|
|
744
|
+
artifact,
|
|
745
|
+
platform: auth.staleLock.platform,
|
|
746
|
+
targetLexical: auth.staleLock.targetLexical,
|
|
747
|
+
targetRealpath: auth.staleLock.targetRealpath,
|
|
748
|
+
holderPid: auth.staleLock.holderPid,
|
|
749
|
+
artifacts: [artifact],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
// Not authorized (rejected, blocked, or not provided).
|
|
755
|
+
if (needed && requiresAuthorized) {
|
|
756
|
+
acc.findings.push(finding("path", reason, "blocked", auth.decision?.reason ?? "missing"));
|
|
757
|
+
acc.refused = true;
|
|
758
|
+
}
|
|
759
|
+
else if (auth.decision !== null) {
|
|
760
|
+
// Provided but rejected/blocked: always a diagnostic issue.
|
|
761
|
+
acc.findings.push(finding("path", reason, "blocked", auth.decision.reason));
|
|
762
|
+
}
|
|
763
|
+
// Not provided and not needed (or inspect mode): nothing to report.
|
|
764
|
+
}
|
|
765
|
+
function deriveNormalizedChecks(input, acc) {
|
|
766
|
+
for (const check of input.normalizedChecks) {
|
|
767
|
+
if (check.status !== "ok") {
|
|
768
|
+
acc.findings.push(finding("normalized", "normalized_check_unhealthy", "blocked", `${check.artifact}:${check.status}`));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// -------------------------------------------------------------------------------------------------
|
|
773
|
+
// Plan assembly.
|
|
774
|
+
// -------------------------------------------------------------------------------------------------
|
|
775
|
+
export function planSessionDoctor(rawInput) {
|
|
776
|
+
const input = validateInput(rawInput);
|
|
777
|
+
const acc = {
|
|
778
|
+
findings: [],
|
|
779
|
+
sessionSpecs: [],
|
|
780
|
+
journalSpecs: [],
|
|
781
|
+
pathSpecs: [],
|
|
782
|
+
compactionSpecs: [],
|
|
783
|
+
refused: false,
|
|
784
|
+
};
|
|
785
|
+
deriveSession(input, acc);
|
|
786
|
+
deriveJournal(input, acc);
|
|
787
|
+
deriveCompaction(input, acc);
|
|
788
|
+
// Paths gate prospective repair actions. Session actions and compaction
|
|
789
|
+
// abandon require an authorized session path; journal actions require an
|
|
790
|
+
// authorized journal path. Inspect mode never performs writes, so paths are
|
|
791
|
+
// only gating in dry/repair.
|
|
792
|
+
const writesPlanned = input.mode !== "inspect";
|
|
793
|
+
const sessionPathNeeded = writesPlanned && (acc.sessionSpecs.length > 0 || acc.compactionSpecs.length > 0);
|
|
794
|
+
const journalPathNeeded = writesPlanned && acc.journalSpecs.length > 0;
|
|
795
|
+
derivePaths(input, acc, sessionPathNeeded, journalPathNeeded);
|
|
796
|
+
deriveNormalizedChecks(input, acc);
|
|
797
|
+
// Deduplicate stale-lock actions (session + journal paths may target the same
|
|
798
|
+
// physical file) before sequence assignment so reported actions stay contiguous.
|
|
799
|
+
const dedupedPathSpecs = dedupStaleLockActions(acc.pathSpecs, acc);
|
|
800
|
+
const orderedSpecs = [
|
|
801
|
+
...acc.sessionSpecs,
|
|
802
|
+
...acc.journalSpecs,
|
|
803
|
+
...dedupedPathSpecs,
|
|
804
|
+
...acc.compactionSpecs,
|
|
805
|
+
];
|
|
806
|
+
const eligibleActions = orderedSpecs.map((spec, index) => deepFreeze({ ...spec, sequence: index }));
|
|
807
|
+
const sortedFindings = Object.freeze(sortFindings(acc.findings).map((f) => deepCopy(f)));
|
|
808
|
+
const refused = acc.refused;
|
|
809
|
+
const reportedActions = refused || input.mode === "inspect" ? [] : eligibleActions;
|
|
810
|
+
const scheduledWrites = input.mode === "repair" && !refused ? eligibleActions.length : 0;
|
|
811
|
+
const requiresReinspection = scheduledWrites > 0;
|
|
812
|
+
// "healthy" permits advisory findings (e.g. compaction_committed /
|
|
813
|
+
// compaction_duplicate); only a blocked finding or an eligible action
|
|
814
|
+
// downgrades the status to "issues".
|
|
815
|
+
const hasBlockingFinding = sortedFindings.some((f) => f.severity === "blocked");
|
|
816
|
+
const clean = !hasBlockingFinding && eligibleActions.length === 0;
|
|
817
|
+
const status = refused ? "refused" : clean ? "healthy" : "issues";
|
|
818
|
+
const exitCode = refused ? 2 : clean ? 0 : 1;
|
|
819
|
+
return deepFreeze({
|
|
820
|
+
schemaVersion: SESSION_DOCTOR_PLAN_SCHEMA_VERSION,
|
|
821
|
+
mode: input.mode,
|
|
822
|
+
sessionId: input.sessionId,
|
|
823
|
+
repairId: input.repairId,
|
|
824
|
+
status,
|
|
825
|
+
exitCode,
|
|
826
|
+
repairable: !refused && eligibleActions.length > 0,
|
|
827
|
+
requiresReinspection,
|
|
828
|
+
scheduledWrites,
|
|
829
|
+
findings: sortedFindings,
|
|
830
|
+
actions: Object.freeze([...reportedActions]),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
//# sourceMappingURL=session-doctor-plan.js.map
|