@dzhechkov/harness-core 0.8.29 → 0.8.31
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/.dz-manifest.json +242 -102
- package/README.md +271 -0
- package/dist/agentdb-index.d.ts +45 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +345 -85
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +170 -0
- package/dist/agentdb-reindex-marker.d.ts.map +1 -0
- package/dist/agentdb-reindex-marker.js +293 -0
- package/dist/agentdb-reindex-marker.js.map +1 -0
- package/dist/agentdb-snapshot-rotation.d.ts +169 -0
- package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
- package/dist/agentdb-snapshot-rotation.js +322 -0
- package/dist/agentdb-snapshot-rotation.js.map +1 -0
- package/dist/agentdb-snapshot.d.ts +126 -0
- package/dist/agentdb-snapshot.d.ts.map +1 -0
- package/dist/agentdb-snapshot.js +195 -0
- package/dist/agentdb-snapshot.js.map +1 -0
- package/dist/apply-leg.d.ts +202 -0
- package/dist/apply-leg.d.ts.map +1 -0
- package/dist/apply-leg.js +975 -0
- package/dist/apply-leg.js.map +1 -0
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +57 -1
- package/dist/book-kb.js.map +1 -1
- package/dist/brain.d.ts +3 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +1 -1
- package/dist/brain.js.map +1 -1
- package/dist/cadence.d.ts +30 -1
- package/dist/cadence.d.ts.map +1 -1
- package/dist/cadence.js +66 -2
- package/dist/cadence.js.map +1 -1
- package/dist/guard.d.ts +14 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +36 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-probe-worker.d.ts.map +1 -1
- package/dist/integration-probe-worker.js +4 -6
- package/dist/integration-probe-worker.js.map +1 -1
- package/dist/integrations-verify.d.ts.map +1 -1
- package/dist/integrations-verify.js +3 -1
- package/dist/integrations-verify.js.map +1 -1
- package/dist/managed-hooks.d.ts +11 -2
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +33 -7
- package/dist/managed-hooks.js.map +1 -1
- package/dist/mutation-gate.d.ts +35 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +49 -9
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +23 -4
- package/dist/patterns.js.map +1 -1
- package/dist/publish.d.ts +21 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js.map +1 -1
- package/dist/repository-origin.d.ts +1 -1
- package/dist/repository-origin.d.ts.map +1 -1
- package/dist/repository-origin.js +7 -1
- package/dist/repository-origin.js.map +1 -1
- package/dist/round-exec.d.ts +41 -0
- package/dist/round-exec.d.ts.map +1 -0
- package/dist/round-exec.js +45 -0
- package/dist/round-exec.js.map +1 -0
- package/dist/round.d.ts +109 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +148 -0
- package/dist/round.js.map +1 -0
- package/dist/run-records.d.ts +8 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +69 -3
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +166 -3
- package/dist/setup.js.map +1 -1
- package/dist/sqlite-read-helpers.d.ts +60 -0
- package/dist/sqlite-read-helpers.d.ts.map +1 -0
- package/dist/sqlite-read-helpers.js +118 -0
- package/dist/sqlite-read-helpers.js.map +1 -0
- package/dist/statusline.d.ts +22 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +43 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +41 -3
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +305 -59
- package/dist/store-counts.js.map +1 -1
- package/dist/store-guard.d.ts +2 -2
- package/dist/store-guard.d.ts.map +1 -1
- package/dist/store-guard.js +6 -0
- package/dist/store-guard.js.map +1 -1
- package/dist/usage.d.ts +55 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +184 -33
- package/dist/usage.js.map +1 -1
- package/dist/vector-tier.d.ts +18 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +26 -2
- package/dist/vector-tier.js.map +1 -1
- package/package.json +7 -6
- package/sbom.json +451 -101
- package/src/agentdb-index.ts +315 -32
- package/src/agentdb-reindex-marker.ts +330 -0
- package/src/agentdb-snapshot-rotation.ts +393 -0
- package/src/agentdb-snapshot.ts +222 -0
- package/src/apply-leg.ts +1046 -0
- package/src/book-kb.ts +64 -1
- package/src/brain.ts +11 -2
- package/src/cadence.ts +87 -2
- package/src/guard.ts +46 -0
- package/src/index.ts +58 -5
- package/src/integration-probe-worker.ts +4 -6
- package/src/integrations-verify.ts +3 -1
- package/src/managed-hooks.ts +32 -7
- package/src/mutation-gate.ts +87 -8
- package/src/operations.ts +89 -0
- package/src/patterns.ts +23 -4
- package/src/publish.ts +23 -0
- package/src/repository-origin.ts +7 -1
- package/src/round-exec.ts +85 -0
- package/src/round.ts +228 -0
- package/src/run-records.ts +77 -2
- package/src/setup.ts +198 -3
- package/src/sqlite-read-helpers.ts +128 -0
- package/src/statusline.ts +61 -9
- package/src/store-counts.ts +391 -61
- package/src/store-guard.ts +8 -2
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, shared helpers for the READ-ONLY sqlite paths (`book-kb.ts`, `agentdb-index.ts`,
|
|
3
|
+
* `patterns.ts`) — feature `readonly-residuals`.
|
|
4
|
+
*
|
|
5
|
+
* Two residuals fixed here (`features/readonly-residuals/01_requirements.md`):
|
|
6
|
+
*
|
|
7
|
+
* FR-1. `applyReadonlyPragmas` closes a handle that was already opened by
|
|
8
|
+
* `openSqliteReadOnly` (`@dzhechkov/memory`) if the follow-up `busy_timeout` pragma throws.
|
|
9
|
+
* Before this helper existed, `book-kb.ts`/`agentdb-index.ts` called `db.pragma(...)`
|
|
10
|
+
* directly on the handle's `db`, outside any `try` — a pragma failure left BOTH the native
|
|
11
|
+
* connection and a tmp-copy directory (ADR-001 ladder rung 2) leaked, because `handle.cleanup()`
|
|
12
|
+
* is only wired into the `close()` this code never reaches.
|
|
13
|
+
*
|
|
14
|
+
* FR-2. `classifySqliteReadFailure` tells apart "the native module isn't installed here"
|
|
15
|
+
* (silent JSON fallback, unchanged behaviour) from "the store file itself is unreadable"
|
|
16
|
+
* (corrupt file, permission failure, …) — the second case used to be swallowed by the same
|
|
17
|
+
* bare `catch {}` in `patterns.ts`, so a corrupted `patterns.sqlite` silently looked like
|
|
18
|
+
* "fewer lessons" instead of "the store broke". `warnOnce` prints that second case exactly
|
|
19
|
+
* once per process, on stderr, without changing the JSON-fallback data path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Minimal shape `applyReadonlyPragmas` needs from an `openSqliteReadOnly` handle. */
|
|
23
|
+
export interface PragmaTarget {
|
|
24
|
+
readonly db: { pragma: (sql: string) => void; close: () => void };
|
|
25
|
+
readonly cleanup: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function closeQuietly(db: { close: () => void }): void {
|
|
29
|
+
try {
|
|
30
|
+
db.close();
|
|
31
|
+
} catch {
|
|
32
|
+
// already failing on the caller's side — a close failure here must not mask the real cause
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn()` and swallow any exception it throws — used for `handle.cleanup()` in the
|
|
38
|
+
* pragma-failure path (fix round 1, MEDIUM #2): a throwing `cleanup()` must not replace the
|
|
39
|
+
* original pragma error the caller is already in the middle of rethrowing.
|
|
40
|
+
*/
|
|
41
|
+
function cleanupQuietly(fn: () => void): void {
|
|
42
|
+
try {
|
|
43
|
+
fn();
|
|
44
|
+
} catch {
|
|
45
|
+
// the pragma failure is the error of record — a cleanup failure here must not mask it
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `db.pragma('busy_timeout = 5000')` on an already-opened read-only handle. If the pragma
|
|
51
|
+
* throws, the connection is closed and `handle.cleanup()` is called (removing a tmp-copy
|
|
52
|
+
* directory, ADR-001 rung 2) BEFORE rethrowing — so a pragma failure never leaks either the
|
|
53
|
+
* native connection or the temporary copy. Both `close()` and `cleanup()` are individually
|
|
54
|
+
* guarded (`closeQuietly` / `cleanupQuietly`, fix round 1, MEDIUM #2) so that EITHER one throwing
|
|
55
|
+
* still lets the original pragma cause reach the caller with the db path attached, and both are
|
|
56
|
+
* still attempted exactly once each regardless of which one (if any) throws.
|
|
57
|
+
*/
|
|
58
|
+
export function applyReadonlyPragmas(handle: PragmaTarget, path: string): void {
|
|
59
|
+
try {
|
|
60
|
+
handle.db.pragma('busy_timeout = 5000'); // harmless on a readonly connection — kept for parity with the writer opener
|
|
61
|
+
} catch (err) {
|
|
62
|
+
closeQuietly(handle.db);
|
|
63
|
+
cleanupQuietly(() => handle.cleanup());
|
|
64
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
65
|
+
throw new Error(`failed to prepare ${path} for reading: ${cause}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Which of the two known catch-all reasons a store-opener failure names. */
|
|
70
|
+
export type SqliteReadFailureKind = 'native-unavailable' | 'store-unreadable';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Loader-specific signatures of "the `better-sqlite3` native module itself could not be loaded"
|
|
74
|
+
* (fix round 1, MEDIUM #1 — narrows the classifier after Codex review found the previous bare
|
|
75
|
+
* `/better-sqlite3/.test(msg)` swallowed `better-sqlite3: file is not a database`, a genuine
|
|
76
|
+
* corruption, as if the module were merely absent). Each pattern names a load-time failure mode,
|
|
77
|
+
* never a runtime/data failure that merely happens to mention the package name.
|
|
78
|
+
*/
|
|
79
|
+
const NATIVE_LOADER_PATTERNS: readonly RegExp[] = [
|
|
80
|
+
/Cannot find module ['"]?better-sqlite3/,
|
|
81
|
+
/better-sqlite3 is unavailable/,
|
|
82
|
+
/Could not locate the bindings file/,
|
|
83
|
+
/was compiled against a different Node\.js version/,
|
|
84
|
+
/invalid ELF header/,
|
|
85
|
+
/NODE_MODULE_VERSION/,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Classify an error caught around `SqliteBackend.openReadOnly` (or an equivalent opener):
|
|
90
|
+
* `'native-unavailable'` only when the failure is genuinely about `better-sqlite3` failing to
|
|
91
|
+
* LOAD — either `code === 'MODULE_NOT_FOUND'` with `better-sqlite3` named in the message or
|
|
92
|
+
* `requireStack` (a bare `MODULE_NOT_FOUND` for an unrelated module is NOT this — it is a
|
|
93
|
+
* different bug and must not be hidden as "native unavailable"), or the message matches one of
|
|
94
|
+
* the {@link NATIVE_LOADER_PATTERNS} native-module-loader signatures (missing module, ABI/version
|
|
95
|
+
* mismatch, corrupt/foreign binary). The existing silent JSON fallback is correct here, nothing
|
|
96
|
+
* changes. Anything else — a corrupt file (`file is not a database`), a permission failure, a
|
|
97
|
+
* genuinely unreadable store — is `'store-unreadable'`: still falls back to JSON (FR-2 does not
|
|
98
|
+
* change returned data), but the caller is expected to warn once via {@link warnOnce}.
|
|
99
|
+
*/
|
|
100
|
+
export function classifySqliteReadFailure(err: unknown): SqliteReadFailureKind {
|
|
101
|
+
const code = (err as { code?: string } | undefined)?.code;
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
const requireStackRaw = (err as { requireStack?: unknown } | undefined)?.requireStack;
|
|
104
|
+
const requireStack = Array.isArray(requireStackRaw) ? requireStackRaw.join('\n') : '';
|
|
105
|
+
|
|
106
|
+
if (code === 'MODULE_NOT_FOUND' && (/better-sqlite3/.test(msg) || /better-sqlite3/.test(requireStack))) {
|
|
107
|
+
return 'native-unavailable';
|
|
108
|
+
}
|
|
109
|
+
if (NATIVE_LOADER_PATTERNS.some((re) => re.test(msg))) return 'native-unavailable';
|
|
110
|
+
return 'store-unreadable';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const warnedKeys = new Set<string>();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Write `text` to stderr the first time this exact `key` is seen in this process, and never
|
|
117
|
+
* again. Used to surface a `'store-unreadable'` classification exactly once per store path per
|
|
118
|
+
* process, instead of once per read call.
|
|
119
|
+
*/
|
|
120
|
+
export function warnOnce(key: string, text: string): void {
|
|
121
|
+
if (warnedKeys.has(key)) return;
|
|
122
|
+
warnedKeys.add(key);
|
|
123
|
+
try {
|
|
124
|
+
process.stderr.write(`${text}\n`);
|
|
125
|
+
} catch {
|
|
126
|
+
// a closed/broken stderr must not break the caller's fallback
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -70,15 +70,30 @@ export interface StatuslineData {
|
|
|
70
70
|
readonly patterns: number;
|
|
71
71
|
/** Absent on parity; missing/unreadable mirror is explicitly unavailable. */
|
|
72
72
|
readonly patternMirror?:
|
|
73
|
-
| {
|
|
73
|
+
| {
|
|
74
|
+
readonly state: 'in-sync' | 'different';
|
|
75
|
+
readonly lexicalMirrorable: number;
|
|
76
|
+
readonly vector: number;
|
|
77
|
+
readonly excluded: { readonly class: number; readonly noise: number };
|
|
78
|
+
}
|
|
74
79
|
| { readonly state: 'unavailable' };
|
|
80
|
+
/** Vector-mirror inventory; absence is explicit instead of being encoded as a missing field. */
|
|
81
|
+
readonly mirror: {
|
|
82
|
+
readonly available: boolean;
|
|
83
|
+
readonly rows: number;
|
|
84
|
+
readonly lessons: number;
|
|
85
|
+
readonly pending: number;
|
|
86
|
+
readonly source: 'agentdb';
|
|
87
|
+
};
|
|
75
88
|
/** Exact lexical-tier availability split; omitted when the enhanced readonly count cannot be established. */
|
|
76
89
|
readonly patternBreakdown?: {
|
|
77
|
-
readonly source: 'lexical';
|
|
90
|
+
readonly source: 'lexical' | 'lexical+mirror';
|
|
78
91
|
readonly active: number;
|
|
79
92
|
readonly quarantined: number;
|
|
80
93
|
/** True once quarantine contains at least one third of the lexical pool. */
|
|
81
94
|
readonly attention: boolean;
|
|
95
|
+
/** Exact label drift by dzId; present only when both tiers expose readable identities. */
|
|
96
|
+
readonly tierParity?: { readonly lexicalOnly: number; readonly mirrorOnly: number };
|
|
82
97
|
/** Absolute lexical/vector quarantine-label delta, present only above the tolerated drift threshold. */
|
|
83
98
|
readonly tierDelta?: number;
|
|
84
99
|
};
|
|
@@ -103,6 +118,8 @@ export interface StatuslineData {
|
|
|
103
118
|
/** Render-ready details for a non-healthy store verdict. */
|
|
104
119
|
export interface StatuslineStoreHealth {
|
|
105
120
|
readonly verdict: Exclude<StoreHealthVerdict, 'ok' | 'no-mark'>;
|
|
121
|
+
/** Explanation for a health verdict that cannot be inferred from counts alone. */
|
|
122
|
+
readonly reason?: string;
|
|
106
123
|
/** Previous maximum for the affected store tier. */
|
|
107
124
|
readonly previousMax?: number;
|
|
108
125
|
/** Snapshot directory used by cold-start recovery guidance. */
|
|
@@ -643,6 +660,8 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
643
660
|
? [basename(rows.vectorSourcePath)] : []),
|
|
644
661
|
],
|
|
645
662
|
};
|
|
663
|
+
} else if (health.verdict === 'busy') {
|
|
664
|
+
storeHealth = { verdict: health.verdict, reason: health.reason };
|
|
646
665
|
} else if (health.verdict === 'source-changed') {
|
|
647
666
|
storeHealth = { verdict: health.verdict };
|
|
648
667
|
}
|
|
@@ -659,14 +678,38 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
659
678
|
// зеркалом не пользуется, он горел бы всегда, а вечно горящий показатель не несёт сведений.
|
|
660
679
|
// Зеркало ЕСТЬ, но прочитать или разложить его не удалось: это отказ инструмента, и он горит.
|
|
661
680
|
const mirrorLessons = storeRows?.vectorLessonRows;
|
|
681
|
+
const mirrorRows = storeRows?.vectorRows;
|
|
662
682
|
const mirrorAbsent = storeRows !== undefined && storeRows.vectorSourcePath === undefined;
|
|
683
|
+
const lexicalMirrorable = storeRows?.lexicalMirrorableRows;
|
|
684
|
+
const excluded = {
|
|
685
|
+
class: storeRows?.lexicalExcludedClassRows ?? 0,
|
|
686
|
+
noise: storeRows?.lexicalExcludedNoiseRows ?? 0,
|
|
687
|
+
};
|
|
688
|
+
const mirrorAvailable = storeRows?.vectorSourcePath !== undefined
|
|
689
|
+
&& typeof mirrorRows === 'number'
|
|
690
|
+
&& mirrorLessons !== undefined;
|
|
663
691
|
const patternMirror: StatuslineData['patternMirror'] = mirrorAbsent
|
|
664
692
|
? undefined
|
|
665
|
-
: storeRows === undefined || storeRows.vectorRows === 'unreadable' ||
|
|
693
|
+
: storeRows === undefined || storeRows.vectorRows === 'unreadable' || storeRows.vectorRows === 'busy'
|
|
694
|
+
|| mirrorLessons === undefined || lexicalMirrorable === undefined
|
|
666
695
|
? { state: 'unavailable' }
|
|
667
|
-
:
|
|
668
|
-
|
|
669
|
-
|
|
696
|
+
: {
|
|
697
|
+
state: mirrorLessons !== lexicalMirrorable ? 'different' : 'in-sync',
|
|
698
|
+
lexicalMirrorable,
|
|
699
|
+
vector: mirrorLessons,
|
|
700
|
+
excluded,
|
|
701
|
+
};
|
|
702
|
+
const mirror: StatuslineData['mirror'] = {
|
|
703
|
+
available: mirrorAvailable,
|
|
704
|
+
rows: typeof mirrorRows === 'number' ? mirrorRows : 0,
|
|
705
|
+
lessons: mirrorAvailable ? mirrorLessons : 0,
|
|
706
|
+
pending: mirrorAbsent && lexicalMirrorable !== undefined
|
|
707
|
+
? lexicalMirrorable
|
|
708
|
+
: mirrorAvailable && lexicalMirrorable !== undefined
|
|
709
|
+
? Math.max(0, lexicalMirrorable - mirrorLessons)
|
|
710
|
+
: 0,
|
|
711
|
+
source: 'agentdb',
|
|
712
|
+
};
|
|
670
713
|
|
|
671
714
|
let patternBreakdown: StatuslineData['patternBreakdown'];
|
|
672
715
|
try {
|
|
@@ -674,14 +717,22 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
674
717
|
&& typeof storeRows.lexicalRows === 'number'
|
|
675
718
|
&& typeof storeRows.lexicalQuarantinedRows === 'number') {
|
|
676
719
|
const quarantined = storeRows.lexicalQuarantinedRows;
|
|
677
|
-
const tierDelta =
|
|
678
|
-
|
|
720
|
+
const tierDelta = mirrorAvailable
|
|
721
|
+
&& typeof storeRows.lexicalMirrorableQuarantinedRows === 'number'
|
|
722
|
+
&& typeof storeRows.vectorQuarantinedRows === 'number'
|
|
723
|
+
? Math.abs(storeRows.lexicalMirrorableQuarantinedRows - storeRows.vectorQuarantinedRows)
|
|
679
724
|
: undefined;
|
|
680
725
|
patternBreakdown = {
|
|
681
|
-
source: 'lexical',
|
|
726
|
+
source: mirrorAvailable ? 'lexical+mirror' : 'lexical',
|
|
682
727
|
active: storeRows.lexicalRows - quarantined,
|
|
683
728
|
quarantined,
|
|
684
729
|
attention: quarantined > 0 && quarantined * 3 >= storeRows.lexicalRows,
|
|
730
|
+
...(storeRows.quarantineTierParity === undefined ? {} : {
|
|
731
|
+
tierParity: {
|
|
732
|
+
lexicalOnly: storeRows.quarantineTierParity.lexicalOnly,
|
|
733
|
+
mirrorOnly: storeRows.quarantineTierParity.mirrorOnly,
|
|
734
|
+
},
|
|
735
|
+
}),
|
|
685
736
|
...(tierDelta !== undefined && tierDelta > QUARANTINE_TIER_DRIFT_TOLERANCE ? { tierDelta } : {}),
|
|
686
737
|
};
|
|
687
738
|
}
|
|
@@ -719,6 +770,7 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
719
770
|
|
|
720
771
|
return {
|
|
721
772
|
patterns,
|
|
773
|
+
mirror,
|
|
722
774
|
...(patternMirror !== undefined ? { patternMirror } : {}),
|
|
723
775
|
...(patternBreakdown !== undefined ? { patternBreakdown } : {}),
|
|
724
776
|
...(usedPatterns !== undefined ? { usedPatterns } : {}),
|