@owlmeans/agent-common 0.1.18-rc.47 → 0.1.18-rc.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-common/SKILL.md +1 -1
- package/build/consts.d.ts +20 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +103 -0
- package/build/consts.js.map +1 -1
- package/build/errors.d.ts +1 -1
- package/build/errors.js +1 -1
- package/build/helpers/conversation.d.ts +0 -9
- package/build/helpers/conversation.d.ts.map +1 -1
- package/build/helpers/conversation.js +0 -28
- package/build/helpers/conversation.js.map +1 -1
- package/build/helpers/truncate.d.ts +10 -0
- package/build/helpers/truncate.d.ts.map +1 -0
- package/build/helpers/truncate.js +29 -0
- package/build/helpers/truncate.js.map +1 -0
- package/build/index.d.ts +4 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/models/pipeline-spec/types.d.ts +57 -0
- package/build/models/pipeline-spec/types.d.ts.map +1 -0
- package/build/models/pipeline-spec/types.js +2 -0
- package/build/models/pipeline-spec/types.js.map +1 -0
- package/build/models/pipeline-spec.d.ts +4 -0
- package/build/models/pipeline-spec.d.ts.map +1 -0
- package/build/models/pipeline-spec.js +171 -0
- package/build/models/pipeline-spec.js.map +1 -0
- package/build/pipeline.d.ts +4 -43
- package/build/pipeline.d.ts.map +1 -1
- package/build/pipeline.js +9 -205
- package/build/pipeline.js.map +1 -1
- package/build/results/types.d.ts +211 -0
- package/build/results/types.d.ts.map +1 -0
- package/build/results/types.js +2 -0
- package/build/results/types.js.map +1 -0
- package/build/results.d.ts +3 -55
- package/build/results.d.ts.map +1 -1
- package/build/results.js +202 -250
- package/build/results.js.map +1 -1
- package/build/types.d.ts +1 -137
- package/build/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/consts.ts +113 -0
- package/src/errors.ts +1 -1
- package/src/helpers/conversation.ts +0 -32
- package/src/helpers/truncate.ts +31 -0
- package/src/index.ts +4 -1
- package/src/models/pipeline-spec/types.ts +58 -0
- package/src/models/pipeline-spec.ts +202 -0
- package/src/pipeline.ts +10 -231
- package/src/results/types.ts +221 -0
- package/src/results.ts +229 -274
- package/src/types.ts +2 -147
- package/tests/pipeline.spec.ts +26 -29
- package/tests/results.spec.ts +24 -26
- package/build/flows.d.ts +0 -21
- package/build/flows.d.ts.map +0 -1
- package/build/flows.js +0 -105
- package/build/flows.js.map +0 -1
- package/src/flows.ts +0 -113
package/src/results.ts
CHANGED
|
@@ -1,315 +1,270 @@
|
|
|
1
1
|
import { DEFAULT_RESULT_COMPACT_CHARS, DEFAULT_RESULT_ENTRY_CHARS, DEFAULT_RESULT_SUMMARY_CHARS, RESULTS_EVERY_STEP, CumulativeFactKind, CumulativeResultSource } from './consts.js'
|
|
2
|
-
import { truncateAt } from './helpers/
|
|
2
|
+
import { truncateAt } from './helpers/truncate.js'
|
|
3
3
|
import { NAMES_ONLY, NOT_VERIFIED, PARTIAL_EMPTY, PARTIAL_WITH_FACTS, SEEDED, SUMMARY_ALONE, SUMMARY_LEFT_OUT, SUMMARY_WITH_FACTS } from './consts.local.js'
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (shape !== '') {
|
|
54
|
-
line += `: ${shape}`
|
|
55
|
-
}
|
|
56
|
-
const attrs = Object.keys(fact.attrs ?? {}).sort(compare)
|
|
57
|
-
.map(key => `${key}: ${oneLine(fact.attrs![key]!)}`)
|
|
58
|
-
if (attrs.length > 0) {
|
|
59
|
-
line += ` (${attrs.join(', ')})`
|
|
60
|
-
}
|
|
61
|
-
if (fact.ref != null && fact.ref !== '') {
|
|
62
|
-
line += ` → \`${oneLine(fact.ref)}\``
|
|
63
|
-
}
|
|
64
|
-
if (fact.specifier != null && fact.specifier !== '') {
|
|
65
|
-
line += ` — import from \`${fact.specifier}\``
|
|
66
|
-
}
|
|
67
|
-
if (fact.path != null && fact.path !== '' && fact.path !== fact.name) {
|
|
68
|
-
line += fact.specifier != null && fact.specifier !== ''
|
|
69
|
-
? ` (file \`${fact.path}\`)`
|
|
70
|
-
: ` — file \`${fact.path}\``
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return line
|
|
74
|
-
}
|
|
75
|
-
|
|
4
|
+
import type {
|
|
5
|
+
CumulativeResultFact, CumulativeResultsHelper, CumulativeResultsSpec, RenderableResultEntry, RenderedResultEntry,
|
|
6
|
+
ResultRenderCaps, StepResultsSpec,
|
|
7
|
+
} from './results/types.js'
|
|
8
|
+
|
|
9
|
+
export const createCumulativeResultsHelper = (): CumulativeResultsHelper => {
|
|
10
|
+
const compare = (a: string, b: string): number => a < b ? -1 : a > b ? 1 : 0
|
|
11
|
+
|
|
12
|
+
const oneLine = (text: string): string => text.replace(/\s+/g, ' ').trim()
|
|
13
|
+
|
|
14
|
+
const factKey = (fact: CumulativeResultFact): string =>
|
|
15
|
+
`${fact.kind}\u0000${fact.name}\u0000${fact.specifier ?? fact.path ?? ''}`
|
|
16
|
+
|
|
17
|
+
/** File facts last — a location is the first thing a budget can spare, a shape the last. */
|
|
18
|
+
const kindRank = (kind: string): number => kind === CumulativeFactKind.File ? 1 : 0
|
|
19
|
+
|
|
20
|
+
const sortFacts = (facts: readonly CumulativeResultFact[]): CumulativeResultFact[] =>
|
|
21
|
+
[...facts].sort((a, b) =>
|
|
22
|
+
kindRank(a.kind) - kindRank(b.kind)
|
|
23
|
+
|| compare(a.kind, b.kind)
|
|
24
|
+
|| compare(a.name, b.name)
|
|
25
|
+
|| compare(a.specifier ?? a.path ?? '', b.specifier ?? b.path ?? ''))
|
|
26
|
+
|
|
27
|
+
const renderResultFact = (fact: CumulativeResultFact): string => {
|
|
28
|
+
let line = `- ${fact.kind} \`${oneLine(fact.name)}\``
|
|
29
|
+
const shape = fact.signature != null && fact.signature.trim() !== ''
|
|
30
|
+
? oneLine(fact.signature)
|
|
31
|
+
: fact.members != null && fact.members.length > 0
|
|
32
|
+
? `{ ${fact.members.map(oneLine).join(', ')} }`
|
|
33
|
+
: ''
|
|
34
|
+
if (shape !== '') {
|
|
35
|
+
line += `: ${shape}`
|
|
36
|
+
}
|
|
37
|
+
const attrs = Object.keys(fact.attrs ?? {}).sort(compare)
|
|
38
|
+
.map(key => `${key}: ${oneLine(fact.attrs![key]!)}`)
|
|
39
|
+
if (attrs.length > 0) {
|
|
40
|
+
line += ` (${attrs.join(', ')})`
|
|
41
|
+
}
|
|
42
|
+
if (fact.ref != null && fact.ref !== '') {
|
|
43
|
+
line += ` → \`${oneLine(fact.ref)}\``
|
|
44
|
+
}
|
|
45
|
+
if (fact.specifier != null && fact.specifier !== '') {
|
|
46
|
+
line += ` — import from \`${fact.specifier}\``
|
|
47
|
+
}
|
|
48
|
+
if (fact.path != null && fact.path !== '' && fact.path !== fact.name) {
|
|
49
|
+
line += fact.specifier != null && fact.specifier !== ''
|
|
50
|
+
? ` (file \`${fact.path}\`)`
|
|
51
|
+
: ` — file \`${fact.path}\``
|
|
52
|
+
}
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
return line
|
|
55
|
+
}
|
|
79
56
|
|
|
80
|
-
const
|
|
57
|
+
const moreFacts = (count: number): string =>
|
|
58
|
+
`- … and ${count} more not listed here for space. They exist: check the files before creating anything similar.`
|
|
81
59
|
|
|
82
|
-
const
|
|
83
|
-
const notes: string[] = []
|
|
84
|
-
const hasFacts = entry.facts.length > 0
|
|
85
|
-
if (entry.partial === true) {
|
|
86
|
-
notes.push(hasFacts ? PARTIAL_WITH_FACTS : PARTIAL_EMPTY)
|
|
87
|
-
}
|
|
88
|
-
if (entry.source === CumulativeResultSource.Seeded) {
|
|
89
|
-
notes.push(SEEDED)
|
|
90
|
-
}
|
|
91
|
-
if (!hasFacts && entry.summary != null) {
|
|
92
|
-
notes.push(NOT_VERIFIED)
|
|
93
|
-
}
|
|
94
|
-
if (compact && (hasFacts || entry.summary != null)) {
|
|
95
|
-
notes.push(hasFacts ? NAMES_ONLY : SUMMARY_LEFT_OUT)
|
|
96
|
-
}
|
|
60
|
+
const moreNames = (count: number): string => `- … and ${count} more`
|
|
97
61
|
|
|
98
|
-
|
|
99
|
-
|
|
62
|
+
const heading = (entry: RenderableResultEntry, compact: boolean): string => {
|
|
63
|
+
const notes: string[] = []
|
|
64
|
+
const hasFacts = entry.facts.length > 0
|
|
65
|
+
if (entry.partial === true) {
|
|
66
|
+
notes.push(hasFacts ? PARTIAL_WITH_FACTS : PARTIAL_EMPTY)
|
|
67
|
+
}
|
|
68
|
+
if (entry.source === CumulativeResultSource.Seeded) {
|
|
69
|
+
notes.push(SEEDED)
|
|
70
|
+
}
|
|
71
|
+
if (!hasFacts && entry.summary != null) {
|
|
72
|
+
notes.push(NOT_VERIFIED)
|
|
73
|
+
}
|
|
74
|
+
if (compact && (hasFacts || entry.summary != null)) {
|
|
75
|
+
notes.push(hasFacts ? NAMES_ONLY : SUMMARY_LEFT_OUT)
|
|
76
|
+
}
|
|
100
77
|
|
|
101
|
-
|
|
102
|
-
const fit = (
|
|
103
|
-
head: string[], lines: string[], tail: string[], max: number, more: (count: number) => string,
|
|
104
|
-
): string => {
|
|
105
|
-
const total = (parts: string[]): number => parts.join('\n').length
|
|
106
|
-
if (total([...head, ...lines, ...tail]) <= max) {
|
|
107
|
-
return [...head, ...lines, ...tail].join('\n')
|
|
78
|
+
return `### ${entry.label}${notes.length > 0 ? ` (${notes.join('; ')})` : ''}`
|
|
108
79
|
}
|
|
109
80
|
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
81
|
+
/** Lines under `head`, as many as fit in `max`, with `more(n)` saying how many did not. */
|
|
82
|
+
const fit = (
|
|
83
|
+
head: string[], lines: string[], tail: string[], max: number, more: (count: number) => string,
|
|
84
|
+
): string => {
|
|
85
|
+
const total = (parts: string[]): number => parts.join('\n').length
|
|
86
|
+
if (total([...head, ...lines, ...tail]) <= max) {
|
|
87
|
+
return [...head, ...lines, ...tail].join('\n')
|
|
116
88
|
}
|
|
117
|
-
kept.push(line)
|
|
118
|
-
}
|
|
119
89
|
|
|
120
|
-
|
|
121
|
-
|
|
90
|
+
// Room for the "more" line is reserved at its widest, so adding it can never break the cap.
|
|
91
|
+
const reserve = more(lines.length).length + 1
|
|
92
|
+
const kept: string[] = []
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (total([...head, ...kept, line, ...tail]) + reserve > max) {
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
kept.push(line)
|
|
98
|
+
}
|
|
122
99
|
|
|
123
|
-
|
|
124
|
-
* An entry's two renderings — in full, and as names only — or two empty strings when it has
|
|
125
|
-
* nothing to say.
|
|
126
|
-
*
|
|
127
|
-
* Deterministic and free of anything that varies between two renderings of the same facts: no
|
|
128
|
-
* timestamp, no run id, no revision. Over its cap an entry DROPS whole facts, in render order, and
|
|
129
|
-
* says how many it dropped; it never cuts one fact's line, because half a signature is a wrong
|
|
130
|
-
* signature. An empty entry that is not partial renders to nothing, so it costs a view nothing.
|
|
131
|
-
*/
|
|
132
|
-
export const renderResultEntry = (
|
|
133
|
-
entry: RenderableResultEntry, caps: ResultRenderCaps = {},
|
|
134
|
-
): { full: string, compact: string } => {
|
|
135
|
-
const maxChars = caps.maxChars ?? DEFAULT_RESULT_ENTRY_CHARS
|
|
136
|
-
const maxCompactChars = caps.maxCompactChars ?? DEFAULT_RESULT_COMPACT_CHARS
|
|
137
|
-
const facts = sortFacts(entry.facts)
|
|
138
|
-
if (facts.length === 0 && entry.summary == null && entry.partial !== true) {
|
|
139
|
-
return { full: '', compact: '' }
|
|
100
|
+
return [...head, ...kept, more(lines.length - kept.length), ...tail].join('\n')
|
|
140
101
|
}
|
|
141
102
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
103
|
+
const renderResultEntry = (
|
|
104
|
+
entry: RenderableResultEntry, caps: ResultRenderCaps = {},
|
|
105
|
+
): RenderedResultEntry => {
|
|
106
|
+
const maxChars = caps.maxChars ?? DEFAULT_RESULT_ENTRY_CHARS
|
|
107
|
+
const maxCompactChars = caps.maxCompactChars ?? DEFAULT_RESULT_COMPACT_CHARS
|
|
108
|
+
const facts = sortFacts(entry.facts)
|
|
109
|
+
if (facts.length === 0 && entry.summary == null && entry.partial !== true) {
|
|
110
|
+
return { full: '', compact: '' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const summary = entry.summary != null && entry.summary.trim() !== ''
|
|
114
|
+
? [`${facts.length > 0 ? SUMMARY_WITH_FACTS : SUMMARY_ALONE} ${oneLine(entry.summary)}`]
|
|
115
|
+
: []
|
|
116
|
+
const full = fit([heading(entry, false)], facts.map(renderResultFact), summary, maxChars, moreFacts)
|
|
146
117
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
118
|
+
const byKind = new Map<string, string[]>()
|
|
119
|
+
for (const fact of facts) {
|
|
120
|
+
const names = byKind.get(fact.kind) ?? []
|
|
121
|
+
if (!names.includes(fact.name)) {
|
|
122
|
+
names.push(fact.name)
|
|
123
|
+
}
|
|
124
|
+
byKind.set(fact.kind, names)
|
|
152
125
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
126
|
+
// One line per kind while the names fit; the rest are counted, never cut.
|
|
127
|
+
const compactHead = heading(entry, true)
|
|
128
|
+
const namesWithin = (reserve: number): { lines: string[], left: number } => {
|
|
129
|
+
const lines: string[] = []
|
|
130
|
+
let left = 0
|
|
131
|
+
for (const [kind, names] of byKind) {
|
|
132
|
+
let line = `- ${kind}:`
|
|
133
|
+
let taken = 0
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
const next = `${line}${taken > 0 ? ',' : ''} \`${oneLine(name)}\``
|
|
136
|
+
if (left > 0 || [compactHead, ...lines, next].join('\n').length + reserve > maxCompactChars) {
|
|
137
|
+
left += 1
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
line = next
|
|
141
|
+
taken += 1
|
|
142
|
+
}
|
|
143
|
+
if (taken > 0) {
|
|
144
|
+
lines.push(line)
|
|
168
145
|
}
|
|
169
|
-
line = next
|
|
170
|
-
taken += 1
|
|
171
|
-
}
|
|
172
|
-
if (taken > 0) {
|
|
173
|
-
lines.push(line)
|
|
174
146
|
}
|
|
147
|
+
|
|
148
|
+
return { lines, left }
|
|
149
|
+
}
|
|
150
|
+
let names = namesWithin(0)
|
|
151
|
+
if (names.left > 0) {
|
|
152
|
+
// Room for the "more" line is reserved at its widest, so adding it can never break the cap.
|
|
153
|
+
names = namesWithin(moreNames(facts.length).length + 1)
|
|
175
154
|
}
|
|
155
|
+
const compact = [
|
|
156
|
+
compactHead, ...names.lines, ...(names.left > 0 ? [moreNames(names.left)] : []),
|
|
157
|
+
].join('\n')
|
|
176
158
|
|
|
177
|
-
return {
|
|
159
|
+
return { full, compact }
|
|
178
160
|
}
|
|
179
|
-
let names = namesWithin(0)
|
|
180
|
-
if (names.left > 0) {
|
|
181
|
-
// Room for the "more" line is reserved at its widest, so adding it can never break the cap.
|
|
182
|
-
names = namesWithin(moreNames(facts.length).length + 1)
|
|
183
|
-
}
|
|
184
|
-
const compact = [
|
|
185
|
-
compactHead, ...names.lines, ...(names.left > 0 ? [moreNames(names.left)] : []),
|
|
186
|
-
].join('\n')
|
|
187
161
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
* first — an entry of a run composed under a step sorts ahead of that step's own entry, which is
|
|
194
|
-
* written only once the composed run has finished.
|
|
195
|
-
*/
|
|
196
|
-
export const compareResultOrder = (a: readonly number[], b: readonly number[]): number => {
|
|
197
|
-
for (let i = 0; i < Math.min(a.length, b.length); ++i) {
|
|
198
|
-
if (a[i] !== b[i]) {
|
|
199
|
-
return a[i]! - b[i]!
|
|
162
|
+
const compareResultOrder = (a: readonly number[], b: readonly number[]): number => {
|
|
163
|
+
for (let i = 0; i < Math.min(a.length, b.length); ++i) {
|
|
164
|
+
if (a[i] !== b[i]) {
|
|
165
|
+
return a[i]! - b[i]!
|
|
166
|
+
}
|
|
200
167
|
}
|
|
201
|
-
}
|
|
202
168
|
|
|
203
|
-
|
|
204
|
-
}
|
|
169
|
+
return b.length - a.length
|
|
170
|
+
}
|
|
205
171
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const at = runId.indexOf('/')
|
|
172
|
+
const rootRunOf = (runId: string): string => {
|
|
173
|
+
const at = runId.indexOf('/')
|
|
209
174
|
|
|
210
|
-
|
|
211
|
-
}
|
|
175
|
+
return at < 0 ? runId : runId.slice(0, at)
|
|
176
|
+
}
|
|
212
177
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
? ''
|
|
220
|
-
: runId.startsWith(`${ledger}/`)
|
|
221
|
-
? runId.slice(ledger.length + 1)
|
|
222
|
-
: runId.slice(rootRunOf(runId).length + 1)
|
|
223
|
-
|
|
224
|
-
return path !== '' ? `${path}/${step}` : step
|
|
225
|
-
}
|
|
178
|
+
const resultLabel = (ledger: string, runId: string, step: string): string => {
|
|
179
|
+
const path = runId === ledger
|
|
180
|
+
? ''
|
|
181
|
+
: runId.startsWith(`${ledger}/`)
|
|
182
|
+
? runId.slice(ledger.length + 1)
|
|
183
|
+
: runId.slice(rootRunOf(runId).length + 1)
|
|
226
184
|
|
|
227
|
-
|
|
228
|
-
if (value == null) {
|
|
229
|
-
return ''
|
|
230
|
-
}
|
|
231
|
-
if (typeof value === 'string') {
|
|
232
|
-
return oneLine(value)
|
|
233
|
-
}
|
|
234
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
235
|
-
return String(value)
|
|
185
|
+
return path !== '' ? `${path}/${step}` : step
|
|
236
186
|
}
|
|
237
|
-
if (Array.isArray(value)) {
|
|
238
|
-
return value.map(flatten).filter(part => part !== '').join(', ')
|
|
239
|
-
}
|
|
240
|
-
if (typeof value === 'object') {
|
|
241
|
-
return Object.keys(value).sort(compare)
|
|
242
|
-
.map(key => [key, flatten((value as Record<string, unknown>)[key])] as const)
|
|
243
|
-
.filter(([, text]) => text !== '')
|
|
244
|
-
.map(([key, text]) => `${key}: ${text}`)
|
|
245
|
-
.join('; ')
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return ''
|
|
249
|
-
}
|
|
250
187
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
188
|
+
const flatten = (value: unknown): string => {
|
|
189
|
+
if (value == null) {
|
|
190
|
+
return ''
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === 'string') {
|
|
193
|
+
return oneLine(value)
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
196
|
+
return String(value)
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
return value.map(flatten).filter(part => part !== '').join(', ')
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === 'object') {
|
|
202
|
+
return Object.keys(value).sort(compare)
|
|
203
|
+
.map(key => [key, flatten((value as Record<string, unknown>)[key])] as const)
|
|
204
|
+
.filter(([, text]) => text !== '')
|
|
205
|
+
.map(([key, text]) => `${key}: ${text}`)
|
|
206
|
+
.join('; ')
|
|
207
|
+
}
|
|
268
208
|
|
|
269
|
-
|
|
270
|
-
|
|
209
|
+
return ''
|
|
210
|
+
}
|
|
271
211
|
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
212
|
+
const renderResultSummary = (
|
|
213
|
+
answer: unknown, maxChars: number = DEFAULT_RESULT_SUMMARY_CHARS,
|
|
214
|
+
): string => {
|
|
215
|
+
const single = typeof answer === 'object' && answer != null && !Array.isArray(answer)
|
|
216
|
+
&& Object.keys(answer).length === 1
|
|
217
|
+
? (answer as Record<string, unknown>)[Object.keys(answer)[0]!]
|
|
218
|
+
: answer
|
|
278
219
|
|
|
279
|
-
|
|
280
|
-
...base,
|
|
281
|
-
...patch,
|
|
282
|
-
...(extractors != null ? { extractors } : {}),
|
|
283
|
-
...(omit != null ? { omit } : {}),
|
|
284
|
-
...(full != null ? { full } : {}),
|
|
220
|
+
return truncateAt(flatten(single), maxChars)
|
|
285
221
|
}
|
|
286
|
-
}
|
|
287
222
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
223
|
+
const union = (left: readonly string[] | undefined, right: readonly string[] | undefined): string[] | undefined =>
|
|
224
|
+
left == null && right == null ? undefined : [...new Set([...(left ?? []), ...(right ?? [])])]
|
|
225
|
+
|
|
226
|
+
const mergeStep = (base: StepResultsSpec | undefined, patch: StepResultsSpec): StepResultsSpec => {
|
|
227
|
+
const extractors = union(base?.extractors, patch.extractors)
|
|
228
|
+
const omit = union(base?.omit, patch.omit)
|
|
229
|
+
const full = base?.full === RESULTS_EVERY_STEP || patch.full === RESULTS_EVERY_STEP
|
|
230
|
+
? RESULTS_EVERY_STEP
|
|
231
|
+
: union(base?.full as string[] | undefined, patch.full as string[] | undefined)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
...patch,
|
|
236
|
+
...(extractors != null ? { extractors } : {}),
|
|
237
|
+
...(omit != null ? { omit } : {}),
|
|
238
|
+
...(full != null ? { full } : {}),
|
|
302
239
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const mergeResultsSpecs = (
|
|
243
|
+
...specs: Array<CumulativeResultsSpec | null | undefined>
|
|
244
|
+
): CumulativeResultsSpec => {
|
|
245
|
+
const merged: CumulativeResultsSpec = {}
|
|
246
|
+
for (const spec of specs) {
|
|
247
|
+
if (spec == null) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const { steps, ...scalars } = spec
|
|
251
|
+
Object.assign(merged, scalars)
|
|
252
|
+
if (steps != null) {
|
|
253
|
+
const into: Record<string, StepResultsSpec> = { ...merged.steps }
|
|
254
|
+
for (const name of Object.keys(steps)) {
|
|
255
|
+
into[name] = mergeStep(into[name], steps[name]!)
|
|
256
|
+
}
|
|
257
|
+
merged.steps = into
|
|
309
258
|
}
|
|
310
|
-
merged.steps = into
|
|
311
259
|
}
|
|
260
|
+
|
|
261
|
+
return merged
|
|
312
262
|
}
|
|
313
263
|
|
|
314
|
-
return
|
|
264
|
+
return {
|
|
265
|
+
factKey, sortFacts, renderResultFact, renderResultEntry, compareResultOrder, rootRunOf, resultLabel,
|
|
266
|
+
renderResultSummary, mergeResultsSpecs,
|
|
267
|
+
}
|
|
315
268
|
}
|
|
269
|
+
|
|
270
|
+
export const cumulativeResultsHelper = createCumulativeResultsHelper()
|