@codyswann/lisa 4.59.1 → 4.59.3
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/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +2 -2
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +6 -6
- package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +16 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/workflows/deploy.yml +31 -9
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +12 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/scripts/check-workflow-package-paths.mjs +37 -3
- package/scripts/lib/workflow-contract-probe.mjs +11 -0
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +137 -9
|
@@ -111,6 +111,7 @@ import path from "node:path";
|
|
|
111
111
|
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
112
112
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
113
113
|
import {
|
|
114
|
+
compareVersions,
|
|
114
115
|
declarationGaps,
|
|
115
116
|
probeReleases,
|
|
116
117
|
readContractDeclaration,
|
|
@@ -489,10 +490,17 @@ export function releaseContains(entries, candidate) {
|
|
|
489
490
|
* @param {Record<string, readonly string[]>} listings - Version to entry list
|
|
490
491
|
* @returns {readonly string[]} One operator-readable line per failure
|
|
491
492
|
*/
|
|
492
|
-
export function findBreakages(groups, listings) {
|
|
493
|
+
export function findBreakages(groups, listings, contracts = {}) {
|
|
493
494
|
return Object.entries(listings).flatMap(([version, entries]) =>
|
|
494
495
|
groups
|
|
495
|
-
.filter(
|
|
496
|
+
.filter(
|
|
497
|
+
group =>
|
|
498
|
+
!group.paths.some(
|
|
499
|
+
p =>
|
|
500
|
+
releaseContains(entries, p) ||
|
|
501
|
+
introducedLater(version, contracts[p])
|
|
502
|
+
)
|
|
503
|
+
)
|
|
496
504
|
.map(
|
|
497
505
|
group =>
|
|
498
506
|
`${version}: ${group.workflow} step "${group.step}" resolves ${group.paths
|
|
@@ -504,6 +512,17 @@ export function findBreakages(groups, listings) {
|
|
|
504
512
|
);
|
|
505
513
|
}
|
|
506
514
|
|
|
515
|
+
/** A declared optional feature may predate its package artifact only on known older releases. */
|
|
516
|
+
function introducedLater(version, contract) {
|
|
517
|
+
return (
|
|
518
|
+
/^\d+\.\d+\.\d+$/.test(version) &&
|
|
519
|
+
typeof contract?.since === "string" &&
|
|
520
|
+
typeof contract.degradation === "string" &&
|
|
521
|
+
contract.degradation.length >= 40 &&
|
|
522
|
+
compareVersions(version, contract.since) < 0
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
507
526
|
/**
|
|
508
527
|
* Run the check.
|
|
509
528
|
* @returns {void}
|
|
@@ -567,7 +586,7 @@ function main() {
|
|
|
567
586
|
});
|
|
568
587
|
|
|
569
588
|
const breakages = [
|
|
570
|
-
...findBreakages(claims.groups, listings),
|
|
589
|
+
...findBreakages(claims.groups, listings, declaration.contracts),
|
|
571
590
|
...gaps.undeclared,
|
|
572
591
|
...gaps.stale,
|
|
573
592
|
...contracts.violations,
|
|
@@ -579,6 +598,19 @@ function main() {
|
|
|
579
598
|
releasesExamined: Object.keys(listings),
|
|
580
599
|
contractProbesExecuted: contracts.executed,
|
|
581
600
|
contractProbesDeferred: contracts.deferred,
|
|
601
|
+
packagePathsDeferred: Object.entries(listings).flatMap(
|
|
602
|
+
([version, entries]) =>
|
|
603
|
+
Object.entries(declaration.contracts)
|
|
604
|
+
.filter(
|
|
605
|
+
([where, contract]) =>
|
|
606
|
+
!releaseContains(entries, where) &&
|
|
607
|
+
introducedLater(version, contract)
|
|
608
|
+
)
|
|
609
|
+
.map(
|
|
610
|
+
([where, contract]) =>
|
|
611
|
+
`${version}: ${where} introduced in ${contract.since}; ${contract.degradation}`
|
|
612
|
+
)
|
|
613
|
+
),
|
|
582
614
|
contractProbesRun: contracts.probed,
|
|
583
615
|
unattributed: claims.unattributed,
|
|
584
616
|
breakages,
|
|
@@ -592,6 +624,8 @@ function main() {
|
|
|
592
624
|
);
|
|
593
625
|
for (const line of contracts.deferred)
|
|
594
626
|
process.stdout.write(` · ${line}\n`);
|
|
627
|
+
for (const line of report.packagePathsDeferred)
|
|
628
|
+
process.stdout.write(` · ${line}\n`);
|
|
595
629
|
for (const line of breakages) process.stdout.write(` ✗ ${line}\n`);
|
|
596
630
|
}
|
|
597
631
|
|
|
@@ -151,6 +151,17 @@ export function readContractDeclaration(declared) {
|
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
for (const [where, entry] of Object.entries(contracts)) {
|
|
154
|
+
if (
|
|
155
|
+
entry?.since !== undefined &&
|
|
156
|
+
(typeof entry.since !== "string" ||
|
|
157
|
+
!/^\d+\.\d+\.\d+$/.test(entry.since) ||
|
|
158
|
+
typeof entry.degradation !== "string" ||
|
|
159
|
+
entry.degradation.length < 40)
|
|
160
|
+
) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`contracts["${where}"]: since needs a release version and degradation must explain the older caller's behavior`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
154
165
|
if (!CONTRACT_KINDS.has(entry?.kind)) {
|
|
155
166
|
throw new Error(
|
|
156
167
|
`contracts["${where}"]: kind must be one of ${[...CONTRACT_KINDS].join(", ")}`
|
|
@@ -997,6 +997,84 @@ export function countDecisiveJobs(jobs) {
|
|
|
997
997
|
return jobs.filter(job => DECISIVE_CONCLUSIONS.has(job?.conclusion)).length;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
+
/** GitHub's explicit job-ceiling diagnostic; unrecognized wording stays unknown. */
|
|
1001
|
+
const TIMEOUT_ANNOTATION = /exceeded the maximum execution time/iu;
|
|
1002
|
+
|
|
1003
|
+
/** Bound diagnostic requests; larger matrices remain unattributed. */
|
|
1004
|
+
const TIMEOUT_PROBE_JOB_CAP = 20;
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Restrict authenticated annotation reads to this repository's check runs.
|
|
1008
|
+
* Preserve Enterprise API prefixes when deriving the relative request path.
|
|
1009
|
+
* @param {object} api - Configured API root and repository.
|
|
1010
|
+
* @param {object} job - Job returned by the Actions API.
|
|
1011
|
+
* @returns {string|null} Repository-scoped API path, or null.
|
|
1012
|
+
*/
|
|
1013
|
+
function checkRunPath(api, job) {
|
|
1014
|
+
const url = job?.check_run_url;
|
|
1015
|
+
if (typeof url !== "string") return null;
|
|
1016
|
+
if (typeof api?.apiUrl !== "string") return null;
|
|
1017
|
+
const prefix = `${api.apiUrl}/repos/${api.repo}/check-runs/`;
|
|
1018
|
+
if (!url.startsWith(prefix) || !/^\d+$/u.test(url.slice(prefix.length)))
|
|
1019
|
+
return null;
|
|
1020
|
+
return url.slice(api.apiUrl.length);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Read explicit timeout evidence without changing the scored verdict.
|
|
1025
|
+
* Missing, failed or partial reads remain unknown; absence does not prove
|
|
1026
|
+
* that an operator cancelled the run.
|
|
1027
|
+
* @param {object} api - API coordinates.
|
|
1028
|
+
* @param {ReadonlyArray<object>|null|undefined} jobs - The scored run's jobs.
|
|
1029
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep.
|
|
1030
|
+
* @returns {Promise<boolean|null>} Timeout found, absent, or unknown.
|
|
1031
|
+
*/
|
|
1032
|
+
export async function readTimeoutEvidence(api, jobs, wait) {
|
|
1033
|
+
if (!Array.isArray(jobs)) return null;
|
|
1034
|
+
const candidates = jobs.filter(
|
|
1035
|
+
job => !DECISIVE_CONCLUSIONS.has(job?.conclusion)
|
|
1036
|
+
);
|
|
1037
|
+
// No indecisive job means nothing was killed part-way, which is a different
|
|
1038
|
+
// statement from "we looked and found nothing" — the caller only asks this
|
|
1039
|
+
// about a run that did not finish.
|
|
1040
|
+
if (candidates.length === 0 || candidates.length > TIMEOUT_PROBE_JOB_CAP)
|
|
1041
|
+
return null;
|
|
1042
|
+
let readEverything = true;
|
|
1043
|
+
for (const job of candidates) {
|
|
1044
|
+
const path = checkRunPath(api, job);
|
|
1045
|
+
if (path === null) {
|
|
1046
|
+
readEverything = false;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
let result;
|
|
1050
|
+
try {
|
|
1051
|
+
result = await apiGet(api, `${path}/annotations?per_page=100`, wait);
|
|
1052
|
+
} catch {
|
|
1053
|
+
readEverything = false;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
// A 404 is `null`, and a body that is not the documented array is a shape
|
|
1057
|
+
// this cannot read. Both are unread, not empty — counting either as "no
|
|
1058
|
+
// annotations" would manufacture the absence of evidence.
|
|
1059
|
+
if (result === null || !Array.isArray(result.body)) {
|
|
1060
|
+
readEverything = false;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (
|
|
1064
|
+
result.body.some(entry =>
|
|
1065
|
+
TIMEOUT_ANNOTATION.test(String(entry?.message ?? ""))
|
|
1066
|
+
)
|
|
1067
|
+
)
|
|
1068
|
+
return true;
|
|
1069
|
+
if (
|
|
1070
|
+
result.body.length >= 100 ||
|
|
1071
|
+
result.headers.get("link")?.includes('rel="next"')
|
|
1072
|
+
)
|
|
1073
|
+
readEverything = false;
|
|
1074
|
+
}
|
|
1075
|
+
return readEverything ? false : null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1000
1078
|
/**
|
|
1001
1079
|
* Reads the scope markers a run published, from its artifact NAMES.
|
|
1002
1080
|
*
|
|
@@ -2039,13 +2117,13 @@ export function formatFinding(finding) {
|
|
|
2039
2117
|
* which run it scores is the same defect one layer down, so the selection is not
|
|
2040
2118
|
* conditional on anything having gone wrong.
|
|
2041
2119
|
*
|
|
2042
|
-
* A scored run that reached NO verdict also names its cause
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2120
|
+
* A scored run that reached NO verdict also names its cause. The job counts
|
|
2121
|
+
* come free from the walk and separate "it never started" from "it started and
|
|
2122
|
+
* stopped"; what they cannot separate is why it stopped, and the earlier
|
|
2123
|
+
* version of this comment claimed they could — it read "7 of 8" as "it ran out
|
|
2124
|
+
* of time", which is true of a ceiling kill and false of an operator cancel.
|
|
2125
|
+
* That guess is now a read: `readTimeoutEvidence` asks GitHub whether it killed
|
|
2126
|
+
* a job at its ceiling, and `describeShortfall` says only what came back.
|
|
2049
2127
|
*
|
|
2050
2128
|
* A DECISIVE conclusion gets no such suffix. `success` and `failure` already say
|
|
2051
2129
|
* what happened, and appending job arithmetic to every green is noise.
|
|
@@ -2053,6 +2131,43 @@ export function formatFinding(finding) {
|
|
|
2053
2131
|
* @param {object|null|undefined} selection - The finding's selection record
|
|
2054
2132
|
* @returns {string|null} One trailing line, or null when there is nothing to say
|
|
2055
2133
|
*/
|
|
2134
|
+
/**
|
|
2135
|
+
* Names why an inconclusive run stopped, to whatever resolution is evidenced.
|
|
2136
|
+
*
|
|
2137
|
+
* Four answers, and the boundary between the last two is the point. A run that
|
|
2138
|
+
* reached no verdict of its own is one of: a duplicate the concurrency group
|
|
2139
|
+
* displaced before it scored anything, a job killed at its `timeout-minutes`
|
|
2140
|
+
* ceiling, an operator pressing cancel, a newer run displacing this one
|
|
2141
|
+
* mid-flight, a runner evicted underneath it, or a `needs:` dependency failing
|
|
2142
|
+
* and taking its dependents with it.
|
|
2143
|
+
*
|
|
2144
|
+
* Only the ceiling kill is a statement about the software or its budget. An
|
|
2145
|
+
* operator cancel is a human decision and says nothing about health, so
|
|
2146
|
+
* reporting the two alike cries wolf: it presents a decision somebody made as a
|
|
2147
|
+
* suite that exhausted its time. It cannot hide a regression in the other
|
|
2148
|
+
* direction — the run is scored and blocks either way, and only the diagnosis
|
|
2149
|
+
* differed — which is why this is a reporting-resolution fix and changes
|
|
2150
|
+
* nothing about which runs are scoreable.
|
|
2151
|
+
*
|
|
2152
|
+
* The residual is named as unattributed rather than as "an operator cancel",
|
|
2153
|
+
* because nothing here proves a human did anything. The evidence available says
|
|
2154
|
+
* a ceiling was hit or does not say so; claiming the negative as a positive
|
|
2155
|
+
* would be the same category error one layer along.
|
|
2156
|
+
* @param {object} selection - The finding's selection record
|
|
2157
|
+
* @returns {string} A clause completing "so it …"
|
|
2158
|
+
*/
|
|
2159
|
+
function describeShortfall(selection) {
|
|
2160
|
+
if (selection.decisiveJobs === 0) return "tested nothing";
|
|
2161
|
+
if (selection.timedOut === true) return "was killed at a time limit";
|
|
2162
|
+
if (selection.timedOut === false)
|
|
2163
|
+
return (
|
|
2164
|
+
"stopped before finishing, with no time-limit evidence on any " +
|
|
2165
|
+
"unfinished job — an operator cancel, a displaced duplicate, an evicted " +
|
|
2166
|
+
"runner and a failed dependency all land here"
|
|
2167
|
+
);
|
|
2168
|
+
return "ran but did not finish, for a reason this gate could not read";
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2056
2171
|
export function formatSelection(selection) {
|
|
2057
2172
|
if (!selection) return null;
|
|
2058
2173
|
const conclusion = selection.conclusion ?? null;
|
|
@@ -2061,7 +2176,7 @@ export function formatSelection(selection) {
|
|
|
2061
2176
|
typeof selection.decisiveJobs === "number" &&
|
|
2062
2177
|
typeof selection.totalJobs === "number";
|
|
2063
2178
|
const cause = counted
|
|
2064
|
-
? ` — ${selection.decisiveJobs} of ${selection.totalJobs} job(s) reached a verdict, so it ${selection
|
|
2179
|
+
? ` — ${selection.decisiveJobs} of ${selection.totalJobs} job(s) reached a verdict, so it ${describeShortfall(selection)}`
|
|
2065
2180
|
: "";
|
|
2066
2181
|
const scored = `↳ scored run ${selection.runId ?? "?"} (${conclusion ?? "no conclusion"}${cause}, ${selection.createdAt ?? "unknown time"})`;
|
|
2067
2182
|
const fallback = selection.fellBack
|
|
@@ -3350,6 +3465,18 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
|
|
|
3350
3465
|
// honest report is "this was scored because nothing better existed", not a
|
|
3351
3466
|
// line contradicting itself.
|
|
3352
3467
|
const skipped = walkedPast.filter(entry => entry.runId !== run.id);
|
|
3468
|
+
const decisiveJobs = countDecisiveJobs(jobs);
|
|
3469
|
+
// Asked only of the one shape that is ambiguous: a run that reached no
|
|
3470
|
+
// verdict of its own yet scored some of its jobs. A decisive run has already
|
|
3471
|
+
// said what happened, and a run with nothing decisive tested nothing — both
|
|
3472
|
+
// are named without this, so neither pays for it. That keeps the common path
|
|
3473
|
+
// at exactly the request count it had before.
|
|
3474
|
+
const timedOut =
|
|
3475
|
+
jobsComplete &&
|
|
3476
|
+
!DECISIVE_CONCLUSIONS.has(run.conclusion ?? null) &&
|
|
3477
|
+
decisiveJobs > 0
|
|
3478
|
+
? await readTimeoutEvidence(api, jobs, wait)
|
|
3479
|
+
: null;
|
|
3353
3480
|
return {
|
|
3354
3481
|
run,
|
|
3355
3482
|
jobs,
|
|
@@ -3360,7 +3487,8 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
|
|
|
3360
3487
|
createdAt: run.created_at ?? null,
|
|
3361
3488
|
// Carried for the SCORED run too, not only the skipped ones, so an
|
|
3362
3489
|
// inconclusive verdict can name its cause. See `formatSelection`.
|
|
3363
|
-
decisiveJobs
|
|
3490
|
+
decisiveJobs,
|
|
3491
|
+
timedOut,
|
|
3364
3492
|
totalJobs: jobs.length,
|
|
3365
3493
|
fellBack: chosen === null && walkedPast.length > 0,
|
|
3366
3494
|
skipped: Object.freeze(skipped.map(entry => Object.freeze(entry))),
|