@namzu/sdk 62.6.1 → 62.7.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 +29 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +5 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +1 -0
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/explore.d.ts.map +1 -1
- package/dist/agents/explore.js +2 -1
- package/dist/agents/explore.js.map +1 -1
- package/dist/compaction/index.d.ts +1 -1
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/model-summary.js +2 -2
- package/dist/compaction/model-summary.js.map +1 -1
- package/dist/compaction/summary.d.ts +16 -1
- package/dist/compaction/summary.d.ts.map +1 -1
- package/dist/compaction/summary.js +25 -2
- package/dist/compaction/summary.js.map +1 -1
- package/dist/config/runtime.d.ts +8 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +15 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +3 -0
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +8 -0
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/manager/agent/child-session.d.ts +12 -0
- package/dist/manager/agent/child-session.d.ts.map +1 -1
- package/dist/manager/agent/child-session.js +29 -0
- package/dist/manager/agent/child-session.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +8 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +13 -2
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +57 -3
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +5 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +5 -1
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +25 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +28 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +5 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
- package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/classifier.js +8 -4
- package/dist/runtime/query/auto-review/classifier.js.map +1 -1
- package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/prompt.js +2 -0
- package/dist/runtime/query/auto-review/prompt.js.map +1 -1
- package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
- package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/script-evidence.js +566 -0
- package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +13 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +37 -0
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +38 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +130 -11
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +7 -1
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +2 -0
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +8 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +7 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +32 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +158 -10
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +48 -1
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +5 -0
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +3 -3
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
- package/dist/runtime/query/prelude-lease.js +22 -0
- package/dist/runtime/query/prelude-lease.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +13 -4
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +29 -0
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
- package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
- package/dist/runtime/query/turn/completion-audit.js +71 -6
- package/dist/runtime/query/turn/completion-audit.js.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.js +578 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
- package/dist/sandbox/glob-segment.d.ts +8 -1
- package/dist/sandbox/glob-segment.d.ts.map +1 -1
- package/dist/sandbox/glob-segment.js +28 -9
- package/dist/sandbox/glob-segment.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -0
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -0
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/store/session-log/turn-outcome.d.ts +37 -0
- package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
- package/dist/store/session-log/turn-outcome.js +77 -0
- package/dist/store/session-log/turn-outcome.js.map +1 -0
- package/dist/tools/builtins/bash.js +1 -1
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +24 -3
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +31 -17
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +92 -27
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/notebook-guard.d.ts +19 -0
- package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
- package/dist/tools/builtins/notebook-guard.js +75 -0
- package/dist/tools/builtins/notebook-guard.js.map +1 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +8 -2
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +14 -1
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +25 -6
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +6 -1
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/outcome-record.d.ts +22 -0
- package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
- package/dist/tools/coordinator/outcome-record.js +91 -0
- package/dist/tools/coordinator/outcome-record.js.map +1 -0
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts +23 -2
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +37 -2
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/tools/untrusted-envelope.d.ts +6 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +22 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +3 -4
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/toolsets/validation-issues.d.ts +27 -0
- package/dist/toolsets/validation-issues.d.ts.map +1 -0
- package/dist/toolsets/validation-issues.js +83 -0
- package/dist/toolsets/validation-issues.js.map +1 -0
- package/dist/turn/memory-recall.d.ts +13 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +18 -0
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +19 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/common/index.d.ts +17 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +8 -1
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/hitl/index.d.ts +22 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/config.d.ts +20 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +3 -3
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/index.d.ts +1 -0
- package/dist/types/session/index.d.ts.map +1 -1
- package/dist/types/session/index.js +1 -0
- package/dist/types/session/index.js.map +1 -1
- package/dist/types/session/outcome.d.ts +95 -0
- package/dist/types/session/outcome.d.ts.map +1 -0
- package/dist/types/session/outcome.js +45 -0
- package/dist/types/session/outcome.js.map +1 -0
- package/dist/types/session/prepare-step.d.ts +2 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/dist/types/session/records.d.ts +167 -2
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +42 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/turn.d.ts +18 -0
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +5 -0
- package/src/agents/examples/SupervisorAgent.ts +1 -0
- package/src/agents/explore.ts +2 -1
- package/src/compaction/index.ts +1 -0
- package/src/compaction/model-summary.ts +2 -2
- package/src/compaction/summary.ts +30 -2
- package/src/config/runtime.ts +15 -0
- package/src/execution/workflow/runtime.ts +4 -0
- package/src/execution/workflow/types.ts +9 -0
- package/src/manager/agent/child-session.ts +36 -0
- package/src/manager/agent/lifecycle.ts +9 -0
- package/src/manager/session/turn-recorder.ts +62 -3
- package/src/pricing/rates.source.json +1 -1
- package/src/prompt/coding-agent-doctrine.ts +5 -3
- package/src/provider/fallback.ts +6 -1
- package/src/provider/retry.ts +51 -0
- package/src/public-runtime.ts +10 -1
- package/src/public-tools.ts +6 -1
- package/src/runtime/query/auto-review/classifier.ts +21 -5
- package/src/runtime/query/auto-review/prompt.ts +2 -0
- package/src/runtime/query/auto-review/script-evidence.ts +618 -0
- package/src/runtime/query/checkpoint.ts +53 -1
- package/src/runtime/query/executor.ts +159 -19
- package/src/runtime/query/finalize-turn.ts +8 -1
- package/src/runtime/query/index.ts +19 -0
- package/src/runtime/query/iteration/index.ts +167 -14
- package/src/runtime/query/iteration/phases/compaction.ts +53 -3
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +3 -3
- package/src/runtime/query/prelude-lease.ts +19 -0
- package/src/runtime/query/prepare-turn.ts +13 -4
- package/src/runtime/query/review-policy.ts +33 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/runtime/query/turn/completion-audit.ts +76 -5
- package/src/runtime/query/turn/outcome-ledger.ts +633 -0
- package/src/sandbox/glob-segment.ts +48 -24
- package/src/store/session-log/index.ts +7 -0
- package/src/store/session-log/turn-outcome.ts +84 -0
- package/src/tools/builtins/bash.ts +1 -1
- package/src/tools/builtins/edit-apply.ts +26 -4
- package/src/tools/builtins/edit.ts +106 -41
- package/src/tools/builtins/glob.ts +1 -1
- package/src/tools/builtins/grep.ts +1 -1
- package/src/tools/builtins/notebook-guard.ts +76 -0
- package/src/tools/builtins/read-render.ts +12 -2
- package/src/tools/builtins/write-file.ts +12 -1
- package/src/tools/coordinator/agent.ts +31 -7
- package/src/tools/coordinator/index.ts +6 -1
- package/src/tools/coordinator/outcome-record.ts +93 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/tools/file-read-tracker.ts +51 -2
- package/src/tools/untrusted-envelope.ts +23 -1
- package/src/toolsets/manager.ts +6 -10
- package/src/toolsets/validation-issues.ts +97 -0
- package/src/turn/memory-recall.ts +28 -0
- package/src/types/agent/base.ts +20 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/common/index.ts +25 -1
- package/src/types/hitl/index.ts +27 -0
- package/src/types/session/config.ts +20 -0
- package/src/types/session/events.ts +3 -3
- package/src/types/session/index.ts +1 -0
- package/src/types/session/outcome.ts +139 -0
- package/src/types/session/prepare-step.ts +2 -0
- package/src/types/session/records.ts +67 -1
- package/src/types/session/turn.ts +19 -0
- package/src/types/tool/index.ts +30 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn's outcome record: what changed in the workspace, what was run to
|
|
3
|
+
* check it, and whether the last word of each check was said about the
|
|
4
|
+
* workspace as the turn left it.
|
|
5
|
+
*
|
|
6
|
+
* It is derived from the turn's own tool calls and, when the host supplies a
|
|
7
|
+
* change detector, from the workspace itself. Never from what the model says.
|
|
8
|
+
* Nothing here blocks or prolongs a turn; the completion audit keeps its own
|
|
9
|
+
* rule. Every uncertainty is reported as uncertainty: a check whose exit status
|
|
10
|
+
* something else may have decided is `masked`, not `passed`; a detector that
|
|
11
|
+
* cannot answer is `null`, never a match.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isReadOnlyCommandLine } from '../../../authorization/readonly-commands.js'
|
|
15
|
+
import { lexShellCommandLine } from '../../../authorization/shell-lexer.js'
|
|
16
|
+
import {
|
|
17
|
+
OUTCOME_DEFAULT_DETECT_TIMEOUT_MS,
|
|
18
|
+
OUTCOME_MAX_CHECKS,
|
|
19
|
+
OUTCOME_MAX_COMMAND_CHARS,
|
|
20
|
+
OUTCOME_MAX_PATHS,
|
|
21
|
+
OUTCOME_MAX_PATH_CHARS,
|
|
22
|
+
type TurnOutcome,
|
|
23
|
+
type TurnOutcomeCheck,
|
|
24
|
+
type TurnOutcomeCheckResult,
|
|
25
|
+
type TurnOutcomeConfig,
|
|
26
|
+
type TurnOutcomeVerdict,
|
|
27
|
+
} from '../../../types/session/outcome.js'
|
|
28
|
+
import type { ToolFacts } from '../../../types/tool/index.js'
|
|
29
|
+
import { WRITING_TOOLS, isVerificationCommand } from './completion-audit.js'
|
|
30
|
+
|
|
31
|
+
/** A finished tool call as the ledger sees it. */
|
|
32
|
+
export interface OutcomeCall {
|
|
33
|
+
readonly toolName: string
|
|
34
|
+
/** The call's parsed input, when it had one. */
|
|
35
|
+
readonly input?: unknown
|
|
36
|
+
readonly isError?: boolean
|
|
37
|
+
/** A hook stopped it before it ran. */
|
|
38
|
+
readonly skipped?: boolean
|
|
39
|
+
/** The tool declared this call read-only (or it only keeps the task list). */
|
|
40
|
+
readonly readOnly?: boolean
|
|
41
|
+
/** What the call did, as typed fields. Absent when a hook replaced the output. */
|
|
42
|
+
readonly facts?: ToolFacts
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A call about to run: enough to tell whether it could change the workspace. */
|
|
46
|
+
export interface PendingOutcomeCall {
|
|
47
|
+
readonly toolName: string
|
|
48
|
+
readonly input?: unknown
|
|
49
|
+
readonly readOnly?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Beyond these the ledger counts but no longer remembers. */
|
|
53
|
+
const MAX_REMEMBERED_PATHS = 1000
|
|
54
|
+
const MAX_REMEMBERED_CHECKS = 200
|
|
55
|
+
|
|
56
|
+
/** Programs that print or search text. A line that only names a check to one of them does not run it. */
|
|
57
|
+
const NOT_A_RUNNER = new Set([
|
|
58
|
+
'echo',
|
|
59
|
+
'printf',
|
|
60
|
+
'cat',
|
|
61
|
+
'grep',
|
|
62
|
+
'rg',
|
|
63
|
+
'sed',
|
|
64
|
+
'awk',
|
|
65
|
+
'head',
|
|
66
|
+
'tail',
|
|
67
|
+
'less',
|
|
68
|
+
'more',
|
|
69
|
+
'man',
|
|
70
|
+
'which',
|
|
71
|
+
'type',
|
|
72
|
+
'command',
|
|
73
|
+
'ls',
|
|
74
|
+
'find',
|
|
75
|
+
'wc',
|
|
76
|
+
'true',
|
|
77
|
+
'false',
|
|
78
|
+
'test',
|
|
79
|
+
'[',
|
|
80
|
+
':',
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
/** Package managers whose `install`-like subcommands name a check word without running it. */
|
|
84
|
+
const PACKAGE_MANAGERS = new Set([
|
|
85
|
+
'npm',
|
|
86
|
+
'pnpm',
|
|
87
|
+
'yarn',
|
|
88
|
+
'bun',
|
|
89
|
+
'pip',
|
|
90
|
+
'pip3',
|
|
91
|
+
'uv',
|
|
92
|
+
'poetry',
|
|
93
|
+
'cargo',
|
|
94
|
+
'composer',
|
|
95
|
+
'gem',
|
|
96
|
+
])
|
|
97
|
+
const INSTALLING = new Set([
|
|
98
|
+
'install',
|
|
99
|
+
'add',
|
|
100
|
+
'i',
|
|
101
|
+
'ci',
|
|
102
|
+
'remove',
|
|
103
|
+
'rm',
|
|
104
|
+
'uninstall',
|
|
105
|
+
'update',
|
|
106
|
+
'upgrade',
|
|
107
|
+
])
|
|
108
|
+
const HELP_OR_VERSION = new Set(['--help', '-h', '--version', '-V', '--list', '--listTests'])
|
|
109
|
+
/** `-v` is verbose to these, a version flag to the rest. */
|
|
110
|
+
const VERBOSE_V = new Set(['pytest', 'py.test', 'mocha', 'vitest', 'jest'])
|
|
111
|
+
|
|
112
|
+
const SNAPSHOT_VERSION = 1
|
|
113
|
+
|
|
114
|
+
interface CheckState {
|
|
115
|
+
key: string
|
|
116
|
+
command: string
|
|
117
|
+
step: number
|
|
118
|
+
result: TurnOutcomeCheckResult
|
|
119
|
+
exitCode?: number
|
|
120
|
+
/** A successful write tool call came after this run. */
|
|
121
|
+
afterWrite: boolean
|
|
122
|
+
/** A call that could change the workspace, and is not a write tool, came after this run. */
|
|
123
|
+
afterUnknown: boolean
|
|
124
|
+
/** The fingerprint taken when it finished; `null` when it could not be taken or was not trustworthy. */
|
|
125
|
+
fingerprint: string | null | undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The part of the ledger a checkpoint keeps, so a resumed turn does not forget what came before it. */
|
|
129
|
+
export interface OutcomeLedgerSnapshot {
|
|
130
|
+
readonly v: typeof SNAPSHOT_VERSION
|
|
131
|
+
readonly started: boolean
|
|
132
|
+
readonly startFingerprint: string | null
|
|
133
|
+
readonly paths: readonly string[]
|
|
134
|
+
readonly writes: number
|
|
135
|
+
/** Absent in a snapshot written before it existed. */
|
|
136
|
+
readonly unseen?: boolean
|
|
137
|
+
/** Absent in a snapshot written before it existed. */
|
|
138
|
+
readonly pathless?: number
|
|
139
|
+
readonly checks: readonly {
|
|
140
|
+
readonly key: string
|
|
141
|
+
readonly command: string
|
|
142
|
+
readonly step: number
|
|
143
|
+
readonly result: TurnOutcomeCheckResult
|
|
144
|
+
readonly exitCode?: number
|
|
145
|
+
readonly afterWrite: boolean
|
|
146
|
+
readonly afterUnknown: boolean
|
|
147
|
+
readonly fingerprint: string | null
|
|
148
|
+
}[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function clip(text: string, max: number): string {
|
|
152
|
+
return text.length > max ? `${text.slice(0, max)}…` : text
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalise(command: string): string {
|
|
156
|
+
return command.trim().replace(/\s+/g, ' ')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function commandOf(input: unknown): string | undefined {
|
|
160
|
+
const command = (input as { command?: unknown } | undefined)?.command
|
|
161
|
+
return typeof command === 'string' ? command : undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pathOf(call: OutcomeCall): string | undefined {
|
|
165
|
+
if (call.facts?.kind === 'edit' || call.facts?.kind === 'write') {
|
|
166
|
+
if (call.facts.path !== undefined) return call.facts.path
|
|
167
|
+
}
|
|
168
|
+
const input = call.input as Record<string, unknown> | undefined
|
|
169
|
+
for (const key of ['path', 'file_path', 'filePath', 'notebook_path']) {
|
|
170
|
+
const value = input?.[key]
|
|
171
|
+
if (typeof value === 'string' && value !== '') return value
|
|
172
|
+
}
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether the line's exit status is its last check's own: every command of the
|
|
178
|
+
* line is a plain command, joined to the others only by `&&`, with nothing
|
|
179
|
+
* before the first, and nothing after the last but a closing `;`. A pipe, a
|
|
180
|
+
* `;` between commands, `||`, a trailing `&`, a group, a nested shell or
|
|
181
|
+
* anything the lexer cannot read to the end lets something else decide the
|
|
182
|
+
* status, and the answer is no. `a && b` exits 0 only when both did.
|
|
183
|
+
*
|
|
184
|
+
* The bash tool does not set `pipefail`, so `pnpm test | tail` exits with
|
|
185
|
+
* `tail`'s status.
|
|
186
|
+
*/
|
|
187
|
+
function exitStatusIsOwn(line: string): boolean {
|
|
188
|
+
const lexed = lexShellCommandLine(line)
|
|
189
|
+
if (!lexed.complete || lexed.opaque || lexed.commands.length === 0) return false
|
|
190
|
+
if (lexed.compoundWords.length > 0) return false
|
|
191
|
+
let cursor = 0
|
|
192
|
+
for (const [index, command] of lexed.commands.entries()) {
|
|
193
|
+
if (command.origin !== 'line' || command.depth !== 0) return false
|
|
194
|
+
const at = line.indexOf(command.text, cursor)
|
|
195
|
+
if (at < 0) return false
|
|
196
|
+
const gap = line.slice(cursor, at).trim()
|
|
197
|
+
if (index === 0 ? gap !== '' : gap !== '&&') return false
|
|
198
|
+
cursor = at + command.text.length
|
|
199
|
+
}
|
|
200
|
+
const tail = line.slice(cursor).trim()
|
|
201
|
+
return tail === '' || tail === ';'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a simple command, as its words, only installs, removes, or asks for
|
|
206
|
+
* help or a version. Such a command names a check word (`lint-staged`,
|
|
207
|
+
* `eslint --version`) and establishes nothing about the work.
|
|
208
|
+
*/
|
|
209
|
+
function establishesNothing(values: readonly string[]): boolean {
|
|
210
|
+
const program = (values[0] ?? '').split('/').pop() ?? ''
|
|
211
|
+
const args = values.slice(1)
|
|
212
|
+
if (args.some((word) => HELP_OR_VERSION.has(word))) return true
|
|
213
|
+
if (args.includes('-v') && !VERBOSE_V.has(program)) return true
|
|
214
|
+
if (PACKAGE_MANAGERS.has(program) && args.slice(0, 3).some((word) => INSTALLING.has(word))) {
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isNamedCheck(text: string, hostIsCheck?: (command: string) => boolean): boolean {
|
|
221
|
+
if (isVerificationCommand(text)) return true
|
|
222
|
+
try {
|
|
223
|
+
return hostIsCheck?.(text) === true
|
|
224
|
+
} catch {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Whether the line holds a command that could change the workspace and is not
|
|
231
|
+
* itself a check: `pnpm test && sed -i s/a/b/ f`. A line the lexer cannot read
|
|
232
|
+
* to the end counts, because what it runs is not known.
|
|
233
|
+
*/
|
|
234
|
+
function changesBeyondChecks(line: string, hostIsCheck?: (command: string) => boolean): boolean {
|
|
235
|
+
const lexed = lexShellCommandLine(line)
|
|
236
|
+
if (!lexed.complete || lexed.opaque) return true
|
|
237
|
+
for (const command of lexed.commands) {
|
|
238
|
+
const values = command.words.slice(command.assignments).map((word) => word.value)
|
|
239
|
+
const program = (values[0] ?? '').split('/').pop() ?? ''
|
|
240
|
+
const isCheck =
|
|
241
|
+
!NOT_A_RUNNER.has(program) &&
|
|
242
|
+
!establishesNothing(values) &&
|
|
243
|
+
isNamedCheck(command.text, hostIsCheck)
|
|
244
|
+
if (isCheck) continue
|
|
245
|
+
if (isReadOnlyCommandLine(command.text)) continue
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Whether some simple command of the line is a recognised check that is not merely named. */
|
|
252
|
+
function runsRecognisedCheck(line: string, hostIsCheck?: (command: string) => boolean): boolean {
|
|
253
|
+
const lexed = lexShellCommandLine(line)
|
|
254
|
+
if (!lexed.complete || lexed.opaque) {
|
|
255
|
+
return !establishesNothing(line.trim().split(/\s+/)) && isNamedCheck(line, hostIsCheck)
|
|
256
|
+
}
|
|
257
|
+
for (const command of lexed.commands) {
|
|
258
|
+
const values = command.words.slice(command.assignments).map((word) => word.value)
|
|
259
|
+
const program = (values[0] ?? '').split('/').pop() ?? ''
|
|
260
|
+
if (NOT_A_RUNNER.has(program)) continue
|
|
261
|
+
if (establishesNothing(values)) continue
|
|
262
|
+
if (isNamedCheck(command.text, hostIsCheck)) return true
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export class OutcomeLedger {
|
|
268
|
+
private readonly config: TurnOutcomeConfig
|
|
269
|
+
private started = false
|
|
270
|
+
private startFingerprint: string | null = null
|
|
271
|
+
private readonly paths = new Set<string>()
|
|
272
|
+
private extraPaths = 0
|
|
273
|
+
/** Successful writes whose path could not be read; each is a change that cannot be named. */
|
|
274
|
+
private pathless = 0
|
|
275
|
+
/** A call ran that could change the workspace and is not a write tool: only the detector could say whether it did. */
|
|
276
|
+
private couldHaveChangedUnseen = false
|
|
277
|
+
private writes = 0
|
|
278
|
+
private readonly checks = new Map<string, CheckState>()
|
|
279
|
+
private unrecorded = false
|
|
280
|
+
private settled: TurnOutcome | undefined
|
|
281
|
+
|
|
282
|
+
constructor(config: TurnOutcomeConfig = {}) {
|
|
283
|
+
this.config = config
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** False once the record is known to be missing what came before a resume. */
|
|
287
|
+
get recorded(): boolean {
|
|
288
|
+
return !this.unrecorded
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The turn resumed from a checkpoint that carries no ledger. What happened
|
|
293
|
+
* before it is unknown, and a record that began as `unchanged` would be a
|
|
294
|
+
* lie, so the turn reports no outcome at all.
|
|
295
|
+
*/
|
|
296
|
+
markUnrecorded(): void {
|
|
297
|
+
this.unrecorded = true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Take back what a checkpoint kept. A snapshot that does not parse leaves the turn unrecorded. */
|
|
301
|
+
restore(snapshot: unknown): void {
|
|
302
|
+
const parsed = parseSnapshot(snapshot)
|
|
303
|
+
if (parsed === undefined) {
|
|
304
|
+
this.unrecorded = true
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
this.started = parsed.started
|
|
308
|
+
this.startFingerprint = parsed.startFingerprint
|
|
309
|
+
for (const path of parsed.paths) this.paths.add(path)
|
|
310
|
+
this.writes = parsed.writes
|
|
311
|
+
this.pathless = parsed.pathless ?? 0
|
|
312
|
+
this.couldHaveChangedUnseen = parsed.unseen === true
|
|
313
|
+
this.checks.clear()
|
|
314
|
+
for (const check of parsed.checks) {
|
|
315
|
+
this.checks.set(check.key, { ...check, fingerprint: check.fingerprint })
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** What a checkpoint should keep. */
|
|
320
|
+
snapshot(): OutcomeLedgerSnapshot {
|
|
321
|
+
return {
|
|
322
|
+
v: SNAPSHOT_VERSION,
|
|
323
|
+
started: this.started,
|
|
324
|
+
startFingerprint: this.startFingerprint,
|
|
325
|
+
paths: [...this.paths],
|
|
326
|
+
writes: this.writes,
|
|
327
|
+
unseen: this.couldHaveChangedUnseen,
|
|
328
|
+
pathless: this.pathless,
|
|
329
|
+
checks: [...this.checks.values()].map((check) => ({
|
|
330
|
+
key: check.key,
|
|
331
|
+
command: check.command,
|
|
332
|
+
step: check.step,
|
|
333
|
+
result: check.result,
|
|
334
|
+
...(check.exitCode !== undefined ? { exitCode: check.exitCode } : {}),
|
|
335
|
+
afterWrite: check.afterWrite,
|
|
336
|
+
afterUnknown: check.afterUnknown,
|
|
337
|
+
fingerprint: check.fingerprint ?? null,
|
|
338
|
+
})),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Whether the call could change the workspace. Anything not provably
|
|
344
|
+
* read-only counts: a tool that does not say, a shell line that is not on
|
|
345
|
+
* the read-only list.
|
|
346
|
+
*/
|
|
347
|
+
private couldChange(call: PendingOutcomeCall): boolean {
|
|
348
|
+
if (call.readOnly === true) return false
|
|
349
|
+
if (call.toolName === 'bash') {
|
|
350
|
+
const command = commandOf(call.input)
|
|
351
|
+
if (command !== undefined && isReadOnlyCommandLine(command)) return false
|
|
352
|
+
}
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async detect(): Promise<string | null> {
|
|
357
|
+
const detect = this.config.detectChanges
|
|
358
|
+
if (detect === undefined) return null
|
|
359
|
+
const deadline = this.config.detectTimeoutMs ?? OUTCOME_DEFAULT_DETECT_TIMEOUT_MS
|
|
360
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
361
|
+
try {
|
|
362
|
+
const asked = Promise.resolve().then(detect)
|
|
363
|
+
// A detector that settles after its deadline must not surface later.
|
|
364
|
+
asked.catch(() => {})
|
|
365
|
+
const late = new Promise<null>((resolve) => {
|
|
366
|
+
timer = setTimeout(() => resolve(null), deadline)
|
|
367
|
+
})
|
|
368
|
+
const answer = await Promise.race([asked, late])
|
|
369
|
+
return typeof answer === 'string' ? answer : null
|
|
370
|
+
} catch {
|
|
371
|
+
return null
|
|
372
|
+
} finally {
|
|
373
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Called with a step's calls before any of them runs. The first call of the
|
|
379
|
+
* turn that could change the workspace takes the starting fingerprint; a turn
|
|
380
|
+
* of provably read-only calls never asks the detector anything.
|
|
381
|
+
*/
|
|
382
|
+
async before(calls: readonly PendingOutcomeCall[]): Promise<void> {
|
|
383
|
+
if (this.started || this.config.detectChanges === undefined) return
|
|
384
|
+
if (!calls.some((call) => this.couldChange(call))) return
|
|
385
|
+
this.started = true
|
|
386
|
+
this.startFingerprint = await this.detect()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Feed the calls of one finished step, in order. */
|
|
390
|
+
async observe(step: number, calls: readonly OutcomeCall[]): Promise<void> {
|
|
391
|
+
// Checks that finished in this step with nothing after them that could
|
|
392
|
+
// change the workspace: the detector, asked once at the end of the step,
|
|
393
|
+
// answers for exactly the state they left.
|
|
394
|
+
let pending: CheckState[] = []
|
|
395
|
+
for (const call of calls) {
|
|
396
|
+
if (call.skipped === true) continue
|
|
397
|
+
const written = WRITING_TOOLS.has(call.toolName)
|
|
398
|
+
if (written && call.isError !== true) {
|
|
399
|
+
if (call.facts?.kind === 'write' && call.facts.operation === 'unchanged') continue
|
|
400
|
+
this.recordWrite(call)
|
|
401
|
+
this.afterEveryCheck('write')
|
|
402
|
+
for (const check of pending) check.fingerprint = null
|
|
403
|
+
pending = []
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
if (written) continue
|
|
407
|
+
const command = call.toolName === 'bash' ? commandOf(call.input) : undefined
|
|
408
|
+
if (command !== undefined && runsRecognisedCheck(command, this.config.isCheck)) {
|
|
409
|
+
this.afterEveryCheck('other')
|
|
410
|
+
for (const check of pending) check.fingerprint = null
|
|
411
|
+
pending = []
|
|
412
|
+
const state = this.recordCheck(step, command, call)
|
|
413
|
+
// `pnpm test && sed -i ...`: the line changed something after the check.
|
|
414
|
+
if (changesBeyondChecks(command, this.config.isCheck)) {
|
|
415
|
+
state.afterUnknown = true
|
|
416
|
+
this.couldHaveChangedUnseen = true
|
|
417
|
+
}
|
|
418
|
+
pending.push(state)
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
if (this.couldChange(call)) {
|
|
422
|
+
this.couldHaveChangedUnseen = true
|
|
423
|
+
this.afterEveryCheck('other')
|
|
424
|
+
for (const check of pending) check.fingerprint = null
|
|
425
|
+
pending = []
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (pending.length > 0 && this.started && this.startFingerprint !== null) {
|
|
429
|
+
const fingerprint = await this.detect()
|
|
430
|
+
for (const check of pending) check.fingerprint = fingerprint
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private afterEveryCheck(kind: 'write' | 'other'): void {
|
|
435
|
+
for (const check of this.checks.values()) {
|
|
436
|
+
if (kind === 'write') check.afterWrite = true
|
|
437
|
+
else check.afterUnknown = true
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private recordWrite(call: OutcomeCall): void {
|
|
442
|
+
this.writes += 1
|
|
443
|
+
const path = pathOf(call)
|
|
444
|
+
if (path === undefined) {
|
|
445
|
+
this.pathless += 1
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
const clipped = clip(path, OUTCOME_MAX_PATH_CHARS)
|
|
449
|
+
if (this.paths.has(clipped)) return
|
|
450
|
+
if (this.paths.size >= MAX_REMEMBERED_PATHS) {
|
|
451
|
+
this.extraPaths += 1
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
this.paths.add(clipped)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private recordCheck(step: number, command: string, call: OutcomeCall): CheckState {
|
|
458
|
+
const key = normalise(command)
|
|
459
|
+
const { result, exitCode } = this.judge(command, call)
|
|
460
|
+
const state: CheckState = {
|
|
461
|
+
key,
|
|
462
|
+
command: clip(key, OUTCOME_MAX_COMMAND_CHARS),
|
|
463
|
+
step,
|
|
464
|
+
result,
|
|
465
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
466
|
+
afterWrite: false,
|
|
467
|
+
afterUnknown: false,
|
|
468
|
+
// Not asked for yet: `undefined` until the step's detector call answers.
|
|
469
|
+
fingerprint: undefined,
|
|
470
|
+
}
|
|
471
|
+
// A failure stands until the same command passes. A later run that never
|
|
472
|
+
// settled (background, timed out) says nothing about the failure.
|
|
473
|
+
const earlier = this.checks.get(key)
|
|
474
|
+
if (earlier?.result === 'failed' && state.result !== 'passed') return earlier
|
|
475
|
+
// Latest run of each distinct command, ordered by when it last ran.
|
|
476
|
+
this.checks.delete(key)
|
|
477
|
+
this.checks.set(key, state)
|
|
478
|
+
if (this.checks.size > MAX_REMEMBERED_CHECKS) {
|
|
479
|
+
// A failing check is never the one forgotten.
|
|
480
|
+
for (const [candidate, held] of this.checks) {
|
|
481
|
+
if (held.result !== 'failed' && candidate !== key) {
|
|
482
|
+
this.checks.delete(candidate)
|
|
483
|
+
break
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return state
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private judge(
|
|
491
|
+
command: string,
|
|
492
|
+
call: OutcomeCall,
|
|
493
|
+
): { result: TurnOutcomeCheckResult; exitCode?: number } {
|
|
494
|
+
const facts = call.facts
|
|
495
|
+
if (facts?.kind === 'failure') return { result: 'failed' }
|
|
496
|
+
if (facts?.kind !== 'command') {
|
|
497
|
+
// No typed record of the run: a hook replaced the output. An error is
|
|
498
|
+
// still an error; anything else says nothing about the exit status.
|
|
499
|
+
return { result: call.isError === true ? 'failed' : 'unsettled' }
|
|
500
|
+
}
|
|
501
|
+
const exitCode = facts.exitCode
|
|
502
|
+
const withCode = exitCode !== undefined ? { exitCode } : {}
|
|
503
|
+
if (facts.background === true || facts.continued === true || facts.timedOut === true) {
|
|
504
|
+
return { result: 'unsettled', ...withCode }
|
|
505
|
+
}
|
|
506
|
+
if (exitCode === undefined) {
|
|
507
|
+
return { result: call.isError === true ? 'failed' : 'unsettled' }
|
|
508
|
+
}
|
|
509
|
+
if (exitCode === 0) {
|
|
510
|
+
if (call.isError === true) return { result: 'unsettled', exitCode }
|
|
511
|
+
return { result: exitStatusIsOwn(command) ? 'passed' : 'masked', exitCode }
|
|
512
|
+
}
|
|
513
|
+
if (!exitStatusIsOwn(command)) return { result: 'masked', exitCode }
|
|
514
|
+
// A non-zero exit the command's own answer (`grep` found nothing) is not a
|
|
515
|
+
// failure, and not a pass either.
|
|
516
|
+
if (facts.exitNote !== undefined && call.isError !== true) {
|
|
517
|
+
return { result: 'unsettled', exitCode }
|
|
518
|
+
}
|
|
519
|
+
return { result: 'failed', exitCode }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** The outcome, once, when the turn ends. `undefined` when the record is not whole. */
|
|
523
|
+
async finish(): Promise<TurnOutcome | undefined> {
|
|
524
|
+
if (this.unrecorded) return undefined
|
|
525
|
+
if (this.settled !== undefined) return this.settled
|
|
526
|
+
let endFingerprint: string | null = null
|
|
527
|
+
if (this.started) endFingerprint = await this.detect()
|
|
528
|
+
const outcome = this.compute(endFingerprint)
|
|
529
|
+
this.settled = outcome
|
|
530
|
+
return outcome
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The record as it stands, on the calls basis, without asking the detector
|
|
535
|
+
* and without settling: the turn goes on and `finish` still answers once.
|
|
536
|
+
*/
|
|
537
|
+
peek(): TurnOutcome | undefined {
|
|
538
|
+
if (this.unrecorded) return undefined
|
|
539
|
+
return this.compute(null)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private compute(endFingerprint: string | null): TurnOutcome {
|
|
543
|
+
const workspace = this.started && this.startFingerprint !== null && endFingerprint !== null
|
|
544
|
+
const moved = workspace && this.startFingerprint !== endFingerprint
|
|
545
|
+
// A write tool that succeeded changed something, even a file the detector
|
|
546
|
+
// cannot see (an ignored one). Counting it is the cautious direction.
|
|
547
|
+
const changed = this.writes > 0 || moved
|
|
548
|
+
// Without the workspace, a shell command or another tool that may have
|
|
549
|
+
// changed files cannot be ruled out, so "nothing changed" is not said.
|
|
550
|
+
const unseen = !workspace && this.couldHaveChangedUnseen
|
|
551
|
+
|
|
552
|
+
const all = [...this.checks.values()]
|
|
553
|
+
const freshOf = (check: CheckState): boolean | undefined => {
|
|
554
|
+
if (check.afterWrite) return false
|
|
555
|
+
if (workspace && check.fingerprint !== undefined) {
|
|
556
|
+
return check.fingerprint === null ? undefined : check.fingerprint === endFingerprint
|
|
557
|
+
}
|
|
558
|
+
return check.afterUnknown ? undefined : true
|
|
559
|
+
}
|
|
560
|
+
const rows = all.map((check) => ({ check, fresh: freshOf(check) }))
|
|
561
|
+
|
|
562
|
+
let verdict: TurnOutcomeVerdict
|
|
563
|
+
const passes = rows.filter((row) => row.check.result === 'passed')
|
|
564
|
+
if (rows.some((row) => row.check.result === 'failed')) verdict = 'failing'
|
|
565
|
+
else if (!changed) verdict = unseen ? 'unknown' : 'unchanged'
|
|
566
|
+
else if (rows.length === 0) verdict = 'unchecked'
|
|
567
|
+
else if (passes.some((row) => row.fresh === true)) verdict = 'checked'
|
|
568
|
+
else if (passes.length > 0 && passes.every((row) => row.fresh === false)) verdict = 'stale'
|
|
569
|
+
else verdict = 'inconclusive'
|
|
570
|
+
|
|
571
|
+
// A write tool whose path could not be read still changed something.
|
|
572
|
+
const total = this.paths.size + this.extraPaths + this.pathless
|
|
573
|
+
const kept = rows.slice(-OUTCOME_MAX_CHECKS)
|
|
574
|
+
const checks: TurnOutcomeCheck[] = kept.map(({ check, fresh }) => ({
|
|
575
|
+
command: check.command,
|
|
576
|
+
step: check.step,
|
|
577
|
+
result: check.result,
|
|
578
|
+
...(check.exitCode !== undefined ? { exitCode: check.exitCode } : {}),
|
|
579
|
+
...(fresh !== undefined ? { fresh } : {}),
|
|
580
|
+
}))
|
|
581
|
+
return {
|
|
582
|
+
verdict,
|
|
583
|
+
basis: workspace ? 'workspace' : 'calls',
|
|
584
|
+
changes: {
|
|
585
|
+
total,
|
|
586
|
+
paths: [...this.paths].slice(0, OUTCOME_MAX_PATHS),
|
|
587
|
+
outsideKnownTools: moved && this.writes === 0,
|
|
588
|
+
},
|
|
589
|
+
checks,
|
|
590
|
+
...(rows.length > OUTCOME_MAX_CHECKS ? { checksTotal: rows.length } : {}),
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function parseSnapshot(value: unknown): OutcomeLedgerSnapshot | undefined {
|
|
596
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
597
|
+
const snap = value as Record<string, unknown>
|
|
598
|
+
if (snap.v !== SNAPSHOT_VERSION || typeof snap.started !== 'boolean') return undefined
|
|
599
|
+
const start = snap.startFingerprint
|
|
600
|
+
if (start !== null && typeof start !== 'string') return undefined
|
|
601
|
+
if (!Array.isArray(snap.paths) || !snap.paths.every((p) => typeof p === 'string'))
|
|
602
|
+
return undefined
|
|
603
|
+
if (typeof snap.writes !== 'number' || !Number.isSafeInteger(snap.writes) || snap.writes < 0) {
|
|
604
|
+
return undefined
|
|
605
|
+
}
|
|
606
|
+
if (snap.unseen !== undefined && typeof snap.unseen !== 'boolean') return undefined
|
|
607
|
+
if (
|
|
608
|
+
snap.pathless !== undefined &&
|
|
609
|
+
(typeof snap.pathless !== 'number' || !Number.isSafeInteger(snap.pathless) || snap.pathless < 0)
|
|
610
|
+
) {
|
|
611
|
+
return undefined
|
|
612
|
+
}
|
|
613
|
+
if (!Array.isArray(snap.checks)) return undefined
|
|
614
|
+
const results = new Set<string>(['passed', 'failed', 'masked', 'unsettled'])
|
|
615
|
+
for (const raw of snap.checks) {
|
|
616
|
+
if (typeof raw !== 'object' || raw === null) return undefined
|
|
617
|
+
const c = raw as Record<string, unknown>
|
|
618
|
+
if (
|
|
619
|
+
typeof c.key !== 'string' ||
|
|
620
|
+
typeof c.command !== 'string' ||
|
|
621
|
+
typeof c.step !== 'number' ||
|
|
622
|
+
typeof c.result !== 'string' ||
|
|
623
|
+
!results.has(c.result) ||
|
|
624
|
+
(c.exitCode !== undefined && typeof c.exitCode !== 'number') ||
|
|
625
|
+
typeof c.afterWrite !== 'boolean' ||
|
|
626
|
+
typeof c.afterUnknown !== 'boolean' ||
|
|
627
|
+
(c.fingerprint !== null && typeof c.fingerprint !== 'string')
|
|
628
|
+
) {
|
|
629
|
+
return undefined
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return snap as unknown as OutcomeLedgerSnapshot
|
|
633
|
+
}
|