@7n/rules 1.49.12 → 1.49.14
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 +13 -0
- package/package.json +2 -2
- package/rules/release/docs/index.md +0 -3
- package/rules/release/docs/release.md +10 -6
- package/rules/test/coverage/fix-worker.mjs +3 -1
- package/scripts/lib/lint-surface/run-fix.mjs +5 -1
- package/skills/git-reconcile/js/docs/orchestrate.md +28 -44
- package/skills/git-reconcile/js/orchestrate.mjs +28 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.49.14] - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- release: @7n/rules@1.49.9
|
|
8
|
+
|
|
9
|
+
## [1.49.13] - 2026-07-26
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Виправлено фактичний шлях worktree, setup failure і spawnSync-діагностику в git-reconcile
|
|
14
|
+
- pass coverage mutation verdict feedback to the next ladder rung
|
|
15
|
+
|
|
3
16
|
## [1.49.12] - 2026-07-26
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules",
|
|
3
|
-
"version": "1.49.
|
|
3
|
+
"version": "1.49.14",
|
|
4
4
|
"description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"rename-yaml-extensions": "bun ./bin/n-rules.js rename-yaml-extensions"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@7n/llm-lib": "2.9.
|
|
68
|
+
"@7n/llm-lib": "2.9.8",
|
|
69
69
|
"@7n/mt": "^0.5.1",
|
|
70
70
|
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
71
71
|
"ajv": "^8.20.0",
|
|
@@ -4,10 +4,7 @@ title: npm/rules/release
|
|
|
4
4
|
resource: npm/rules/release/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# npm/rules/release
|
|
8
|
-
|
|
9
7
|
| Файл | Тип |
|
|
10
8
|
| ------------------------- | --------- |
|
|
11
9
|
| [change.mjs](change.md) | JS Module |
|
|
12
|
-
| [main.mjs](main.md) | JS Module |
|
|
13
10
|
| [release.mjs](release.md) | JS Module |
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: release.mjs
|
|
4
4
|
resource: npm/rules/release/release.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 496b51f4
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
8
|
tier: cloud-min
|
|
9
9
|
score: 100
|
|
@@ -12,18 +12,22 @@ docgen:
|
|
|
12
12
|
|
|
13
13
|
## Огляд
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`release` агрегує per-workspace change-файли у version-bump і CHANGELOG, комітить результат, ставить тег `<name>@<version>` та видаляє використані change-файли. Запускається у CI на `main` за `n-rules-release-design`, варіант A, і сам нічого не публікує.
|
|
16
|
+
|
|
17
|
+
`runReleaseCli` — CLI wrapper для запуску релізного процесу. Він потрібен, щоб ініціювати `release` із командного рядка.
|
|
16
18
|
|
|
17
19
|
## Поведінка
|
|
18
20
|
|
|
19
|
-
`release`
|
|
21
|
+
`release` послідовно обходить workspaces, збирає change-файли з їхніх директорій, визначає новий bump, оновлює версію в маніфесті, дописує секцію в CHANGELOG і прибирає використані change-файли. Якщо для пакета немає явних change-файлів, але є коміти, він все одно формує релізний запис на основі цього джерела змін.
|
|
22
|
+
|
|
23
|
+
Результат `release` — список фактично релізнутих workspace-ів із новими версіями; цей список далі йде в commit-back і створення анотованих тегів `\<name>@\<version>`. Якщо релізу для workspace нема, він не потрапляє до вихідного списку й не отримує тег.
|
|
20
24
|
|
|
21
|
-
`runReleaseCli`
|
|
25
|
+
`runReleaseCli` лише запускає `release` і передає в GitHub Actions перелік workspace-ів, для яких справді з’явилася нова версія.
|
|
22
26
|
|
|
23
27
|
## Публічний API
|
|
24
28
|
|
|
25
|
-
- release — агрегує per-workspace change-файли у version-bump і CHANGELOG,
|
|
26
|
-
- runReleaseCli — CLI-entry для запуску `release
|
|
29
|
+
- release — агрегує per-workspace change-файли у version-bump і CHANGELOG, комітить зміни, ставить тег `<name>@<version>` та видаляє use-up change-файли; спирається на package.json.
|
|
30
|
+
- runReleaseCli — CLI-entry point для ручного запуску `release` без окремої релізної логіки.
|
|
27
31
|
|
|
28
32
|
## Гарантії поведінки
|
|
29
33
|
|
|
@@ -90,6 +90,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
|
|
|
90
90
|
const touchedFiles = []
|
|
91
91
|
/** @type {Array<{provider: string, hook: string, files: string[], error: string}>} */
|
|
92
92
|
const failed = []
|
|
93
|
+
let feedback = null
|
|
93
94
|
const deferred = []
|
|
94
95
|
/**
|
|
95
96
|
* Викликає опційний fix-hook провайдера, збирає touchedFiles; виняток хука не
|
|
@@ -104,6 +105,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
|
|
|
104
105
|
try {
|
|
105
106
|
const res = await provider[hook]({ ...args, cwd: ctx.cwd, ctx: hookCtx(ctx, deadlineAt) })
|
|
106
107
|
touchedFiles.push(...(res?.touchedFiles ?? []))
|
|
108
|
+
if (res?.feedback?.previousError) feedback = res.feedback
|
|
107
109
|
deferred.push(...(res?.deferred ?? []))
|
|
108
110
|
for (const failure of res?.failed ?? []) {
|
|
109
111
|
const error = failure?.error ?? 'невідома причина'
|
|
@@ -135,5 +137,5 @@ export async function fixWorker(violations, ctx, deps = {}) {
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
return { touchedFiles, failed, deferred }
|
|
140
|
+
return { touchedFiles, failed, deferred, feedback }
|
|
139
141
|
}
|
|
@@ -454,6 +454,10 @@ async function runRung(rung, worker, violations, feedback, rungDeps) {
|
|
|
454
454
|
|
|
455
455
|
const vetoed = after.length === 0 && !error && (collateralAll.length > 0 || brokenTest !== null)
|
|
456
456
|
const touchedFiles = workerResult?.touchedFiles ?? []
|
|
457
|
+
// Provider може повернути quality-verdict без worker exception (наприклад,
|
|
458
|
+
// generated coverage test зелений у Vitest, але не вбив target mutant). Це
|
|
459
|
+
// не змінює retry policy; лише дає наступному ladder rung-у конкретний feedback.
|
|
460
|
+
const workerFeedback = workerResult?.feedback?.previousError ?? null
|
|
457
461
|
|
|
458
462
|
if (after.length === 0 && !error && !vetoed) {
|
|
459
463
|
log(` ✅ ${rung.tier} (${rung.model}): ${ruleId}/${concernName}\n`)
|
|
@@ -501,7 +505,7 @@ async function runRung(rung, worker, violations, feedback, rungDeps) {
|
|
|
501
505
|
outcome: {
|
|
502
506
|
action: decideAfterFailure(rung, error),
|
|
503
507
|
violations: after.length > 0 ? after : violations,
|
|
504
|
-
feedback: { previousModel: rung.model, previousError: error ?? silentFailureNote }
|
|
508
|
+
feedback: { previousModel: rung.model, previousError: error ?? workerFeedback ?? silentFailureNote }
|
|
505
509
|
}
|
|
506
510
|
}
|
|
507
511
|
}
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: orchestrate.mjs
|
|
4
4
|
resource: npm/skills/git-reconcile/js/orchestrate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 6e9cb156
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
8
|
tier: cloud-avg
|
|
9
9
|
score: 100
|
|
@@ -21,7 +21,7 @@ docgen:
|
|
|
21
21
|
|
|
22
22
|
Для review-кандидатів `buildTriagePrompt` формує обмежене завдання для LLM: модель отримує вже пораховані Git-факти й має повернути лише JSON-рішення. `callRunner` викликає обраний runner, `parseDecisionEnvelope` дістає структуровану відповідь, а `validateTriageOutcome` приймає тільки повний і узгоджений набір рішень для поточного batch. `callWithValidatedFallback` спершу пробує дешевший рівень, а дорожчий використовує лише після конкретного validation failure.
|
|
23
23
|
|
|
24
|
-
Під час матеріалізації рішення source переноситься у керований worktree. `branchSlug` забезпечує передбачувані rescue
|
|
24
|
+
Під час матеріалізації рішення source переноситься у керований worktree. `branchSlug` забезпечує передбачувані rescue-гілки без крайового дефіса після скорочення, а `ensureLocalWorktreeExclude` не дає службовим worktree забруднювати root `git status`. Після `@7n/mt` actual checkout визначається через `git worktree list --porcelain`, тому collision sanitized-каталогу не веде до реконструйованого cwd. Setup failure повертається як `failed` лише для цієї PR-групи з branch, фактичним worktree та `spawnSync`-діагностикою; source і forensic checkout не очищуються. Якщо cherry-pick стає порожнім, `skipEmptyCherryPick` дозволяє skip лише для доведеного semantic no-op, а `finishCherryPick` завершує активний перенос без прийняття неперевіреного Git-стану. `hasChangesFromBase` відсікає PR без реального tree diff.
|
|
25
25
|
|
|
26
26
|
Behavior-gates будуються навколо baseline: `captureBehaviorBaseline` фіксує стан чистої `origin/<baseBranch>` із repository Git policy, а `captureCachedBehaviorBaseline` перевикористовує цей результат у межах одного прогону для однакової бази. `validateBehaviorState` перевіряє Git-консистентність, scoped lint/docs і test outcome; правила запуску тестів і скриптів беруться з `package.json`. `testFailureSignatures` нормалізує failures, а `acceptsTestOutcome` дозволяє red baseline тільки без нових failures. Якщо валідація падає через типовий formatting, CSpell, docs або changelog-дефект, `remediateBehaviorState` запускає canonical fixers перед ескалацією LLM.
|
|
27
27
|
|
|
@@ -31,58 +31,42 @@ Behavior-gates будуються навколо baseline: `captureBehaviorBasel
|
|
|
31
31
|
|
|
32
32
|
## Публічний API
|
|
33
33
|
|
|
34
|
-
- createPhaseProgress — Створює ANSI-free snapshot progress для однієї фази.
|
|
35
|
-
формат у TTY/CI не засмічує captured output cursor-control кодами, а
|
|
36
|
-
heartbeat показує elapsed time довгих LLM-етапів.
|
|
34
|
+
- createPhaseProgress — Створює ANSI-free snapshot progress для однієї фази.
|
|
37
35
|
- parseWorktrees — Парсить `git worktree list --porcelain` у branch→path.
|
|
38
|
-
- dedupeRefs — Дедуплікує local/remote refs одного commit
|
|
39
|
-
worktree-protection локального ref переноситься у запис.
|
|
36
|
+
- dedupeRefs — Дедуплікує local/remote refs одного commit.
|
|
40
37
|
- conflictFiles — Витягає конфліктні файли з `git merge-tree`.
|
|
41
|
-
- inventoryRepository — Збирає детермінований Git inventory.
|
|
42
|
-
|
|
43
|
-
- buildTriagePrompt — Формує bounded semantic-triage prompt. Git-факти вже пораховані JS; модель
|
|
44
|
-
не виконує shell-команди й повертає лише JSON-рішення.
|
|
38
|
+
- inventoryRepository — Збирає детермінований Git inventory.
|
|
39
|
+
- buildTriagePrompt — Формує bounded semantic-triage prompt.
|
|
45
40
|
- parseDecisionEnvelope — Витягає JSON object із чистої або fenced відповіді.
|
|
46
41
|
- callRunner — Викликає вибраний LLM runner для одного bounded-завдання.
|
|
47
|
-
- callWithValidatedFallback — Виконує
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
- changedNonCodeDirectories — Повертає директорії non-code змін для фінального domain lint.
|
|
65
|
-
- remediateBehaviorState — Запускає canonical fixers у worktree до ескалації min→max. Це прибирає
|
|
66
|
-
formatting/CSpell/doc/changelog дефекти без повторного behavioral LLM.
|
|
67
|
-
- captureBehaviorBaseline — Фіксує test baseline на чистій policy base branch до перенесення source.
|
|
68
|
-
- captureCachedBehaviorBaseline — Повторно використовує test baseline однієї policy base branch між PR-групами.
|
|
69
|
-
Залежності все одно встановлюються в кожному окремому worktree.
|
|
70
|
-
- validateBehaviorState — Додає до Git-state validation test script із репозиторію і changelog gate.
|
|
71
|
-
Саме ці докази вирішують, чи приймати min-результат або ескалювати на max.
|
|
72
|
-
- validateFinalProjectGates — Фінальний domain gate охоплює non-code зміни, зокрема workflows, dependency
|
|
73
|
-
manifests і правила. Code directories уже пройшли scoped lint і tests.
|
|
74
|
-
- cleanupSource — Видаляє точний source після Git-доказу неактуальності або успішного
|
|
75
|
-
перенесення. Protected/open-PR refs не потрапляють у цей крок.
|
|
42
|
+
- callWithValidatedFallback — Виконує min, валідує результат і викликає max лише після validation failure.
|
|
43
|
+
- validateTriageOutcome — Структурно перевіряє triage-рішення.
|
|
44
|
+
- branchSlug — Перетворює title/ref на валідний короткий branch slug.
|
|
45
|
+
- ensureLocalWorktreeExclude — Додає `.worktrees/` до локального Git exclude без tracked-змін.
|
|
46
|
+
- skipEmptyCherryPick — Пропускає лише підтверджений empty cherry-pick.
|
|
47
|
+
- finishCherryPick — Завершує активний cherry-pick.
|
|
48
|
+
- hasChangesFromBase — Перевіряє реальний tree diff, а не commits ahead.
|
|
49
|
+
- testFailureSignatures — Витягає стабільні Vitest failure identifiers.
|
|
50
|
+
- acceptsTestOutcome — Дозволяє red baseline лише без нових failures.
|
|
51
|
+
- sourceDirectories — Зводить code paths до найвужчих директорій для scoped gates.
|
|
52
|
+
- changedNonCodeDirectories — Повертає директорії non-code змін для domain lint.
|
|
53
|
+
- remediateBehaviorState — Запускає canonical fixers перед behavioral max fallback.
|
|
54
|
+
- captureBehaviorBaseline — Фіксує test baseline на чистій policy base branch.
|
|
55
|
+
- captureCachedBehaviorBaseline — Кешує test baseline між PR-групами.
|
|
56
|
+
- validateBehaviorState — Додає Git-state validation, tests і changelog gate.
|
|
57
|
+
- validateFinalProjectGates — Перевіряє фінальні non-code domain gates.
|
|
58
|
+
- cleanupSource — Видаляє тільки доказово безпечний точний source.
|
|
76
59
|
- formatReport — Формує deterministic report.
|
|
77
|
-
- runWithConcurrency — Виконує async jobs із bounded concurrency та стабільним
|
|
60
|
+
- runWithConcurrency — Виконує async jobs із bounded concurrency та стабільним output.
|
|
78
61
|
- normalizePrConcurrency — Нормалізує bounded concurrency PR-фази.
|
|
79
|
-
- runGitReconcileOrchestrator —
|
|
62
|
+
- runGitReconcileOrchestrator — Координує inventory, triage, PR і cleanup.
|
|
80
63
|
|
|
81
64
|
## Гарантії поведінки
|
|
82
65
|
|
|
83
66
|
- Progress є append-only та не містить ANSI cursor-control sequences.
|
|
84
67
|
- Не більше чотирьох PR-груп виконуються одночасно; типовий ліміт — три.
|
|
85
68
|
- Canonical fixers мають пріоритет перед behavioral `max` fallback.
|
|
86
|
-
- Cleanup починається лише після завершення всіх PR jobs і не видаляє
|
|
87
|
-
|
|
69
|
+
- Cleanup починається лише після завершення всіх PR jobs і не видаляє protected/open-PR/failed sources.
|
|
70
|
+
- `spawnSync.error`, зокрема `ENOENT`, не губиться в command diagnostics.
|
|
71
|
+
- Setup failure зберігає forensic worktree і не зупиняє незалежні PR-групи.
|
|
88
72
|
- Test baseline кешується за OID policy base branch у межах одного прогону.
|
|
@@ -138,7 +138,7 @@ export function createPhaseProgress(args) {
|
|
|
138
138
|
* @param {string} cwd робочий каталог
|
|
139
139
|
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
140
140
|
* @param {{ allowFailure?: boolean, input?: string }} [options] режим помилки та stdin
|
|
141
|
-
* @returns {{ status: number, stdout: string, stderr: string }} результат
|
|
141
|
+
* @returns {{ status: number, stdout: string, stderr: string, error: string }} результат
|
|
142
142
|
*/
|
|
143
143
|
function run(command, args, cwd, spawnFn, options = {}) {
|
|
144
144
|
const childEnv = { ...env, GIT_EDITOR: 'true' }
|
|
@@ -153,11 +153,12 @@ function run(command, args, cwd, spawnFn, options = {}) {
|
|
|
153
153
|
const normalized = {
|
|
154
154
|
status: result.status ?? 1,
|
|
155
155
|
stdout: result.stdout ?? '',
|
|
156
|
-
stderr: result.stderr ?? ''
|
|
156
|
+
stderr: result.stderr ?? '',
|
|
157
|
+
error: result.error ? `${result.error.name ?? 'Error'}${result.error.code ? ` ${result.error.code}` : ''}: ${result.error.message}` : ''
|
|
157
158
|
}
|
|
158
159
|
if (!options.allowFailure && normalized.status !== 0) {
|
|
159
160
|
throw new Error(
|
|
160
|
-
`${command} ${args.join(' ')} → exit ${normalized.status}: ${normalized.stderr || normalized.stdout}`
|
|
161
|
+
`${command} ${args.join(' ')} → exit ${normalized.status}: ${normalized.stderr || normalized.stdout || normalized.error}`
|
|
161
162
|
)
|
|
162
163
|
}
|
|
163
164
|
return normalized
|
|
@@ -666,6 +667,7 @@ export function branchSlug(value) {
|
|
|
666
667
|
.replaceAll(/[^a-z0-9]+/g, '-')
|
|
667
668
|
.replaceAll(/^-|-$/g, '')
|
|
668
669
|
.slice(0, 40)
|
|
670
|
+
.replaceAll(/^-|-$/g, '')
|
|
669
671
|
return slug || 'change'
|
|
670
672
|
}
|
|
671
673
|
|
|
@@ -706,12 +708,22 @@ function chooseBranch(title, cwd, spawnFn) {
|
|
|
706
708
|
function createReconcileWorktree(title, source, cwd, spawnFn) {
|
|
707
709
|
const baseRef = policyBaseRef(cwd)
|
|
708
710
|
const branch = chooseBranch(title, cwd, spawnFn)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
711
|
+
let worktreeCwd
|
|
712
|
+
try {
|
|
713
|
+
run('npx', ['@7n/mt', 'worktree', 'create', branch, `git-reconcile: ${source}`], cwd, spawnFn)
|
|
714
|
+
const worktrees = parseWorktrees(git(['worktree', 'list', '--porcelain'], cwd, spawnFn).stdout)
|
|
715
|
+
worktreeCwd = worktrees.get(`refs/heads/${branch}`)
|
|
716
|
+
if (!worktreeCwd) throw new Error(`@7n/mt не зареєстрував worktree для ${branch}`)
|
|
717
|
+
git(['switch', '--detach', baseRef], worktreeCwd, spawnFn)
|
|
718
|
+
git(['branch', '-f', branch, baseRef], worktreeCwd, spawnFn)
|
|
719
|
+
git(['switch', branch], worktreeCwd, spawnFn)
|
|
720
|
+
return { branch, cwd: worktreeCwd }
|
|
721
|
+
} catch (error) {
|
|
722
|
+
const setupError = error instanceof Error ? error : new Error(String(error))
|
|
723
|
+
setupError.branch = branch
|
|
724
|
+
if (worktreeCwd) setupError.worktree = worktreeCwd
|
|
725
|
+
throw setupError
|
|
726
|
+
}
|
|
715
727
|
}
|
|
716
728
|
|
|
717
729
|
/**
|
|
@@ -1225,11 +1237,12 @@ async function createPullRequest(args) {
|
|
|
1225
1237
|
return { status: 'kept', error: 'LLM не вибрала жодного валідного commit oid' }
|
|
1226
1238
|
}
|
|
1227
1239
|
|
|
1228
|
-
|
|
1229
|
-
const worktree = createReconcileWorktree(group.title, source, rootCwd, spawnFn)
|
|
1240
|
+
let worktree
|
|
1230
1241
|
let createdPr
|
|
1231
|
-
log(`🌿 ${source} → ${worktree.branch}`)
|
|
1232
1242
|
try {
|
|
1243
|
+
onProgress('worktree')
|
|
1244
|
+
worktree = createReconcileWorktree(group.title, source, rootCwd, spawnFn)
|
|
1245
|
+
log(`🌿 ${source} → ${worktree.branch}`)
|
|
1233
1246
|
const sourceMayChangeCode = (candidate.changedFiles ?? []).some(path => SOURCE_CODE_RE.test(path))
|
|
1234
1247
|
let baseline = null
|
|
1235
1248
|
if (sourceMayChangeCode) {
|
|
@@ -1318,11 +1331,12 @@ async function createPullRequest(args) {
|
|
|
1318
1331
|
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1319
1332
|
return { status: 'pr-created', url: createdPr, branch: worktree.branch }
|
|
1320
1333
|
} catch (error) {
|
|
1334
|
+
const setup = /** @type {{branch?:string,worktree?:string}} */ (error)
|
|
1321
1335
|
return {
|
|
1322
1336
|
status: 'failed',
|
|
1323
1337
|
error: error instanceof Error ? error.message : String(error),
|
|
1324
|
-
branch: worktree.branch,
|
|
1325
|
-
worktree: worktree.
|
|
1338
|
+
branch: worktree?.branch ?? setup.branch,
|
|
1339
|
+
worktree: worktree?.cwd ?? setup.worktree,
|
|
1326
1340
|
...(createdPr && { url: createdPr })
|
|
1327
1341
|
}
|
|
1328
1342
|
}
|