@dzhechkov/harness-core 0.8.11 → 0.8.21

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.
Files changed (205) hide show
  1. package/.dz-manifest.json +399 -139
  2. package/LICENSE +13 -0
  3. package/README.md +420 -6
  4. package/dist/agents-policy.d.ts +15 -1
  5. package/dist/agents-policy.d.ts.map +1 -1
  6. package/dist/agents-policy.js +27 -1
  7. package/dist/agents-policy.js.map +1 -1
  8. package/dist/amendment-trace.d.ts +72 -0
  9. package/dist/amendment-trace.d.ts.map +1 -1
  10. package/dist/amendment-trace.js +385 -17
  11. package/dist/amendment-trace.js.map +1 -1
  12. package/dist/backlog-public.d.ts +153 -0
  13. package/dist/backlog-public.d.ts.map +1 -0
  14. package/dist/backlog-public.js +415 -0
  15. package/dist/backlog-public.js.map +1 -0
  16. package/dist/backlog-transitions.d.ts +48 -0
  17. package/dist/backlog-transitions.d.ts.map +1 -0
  18. package/dist/backlog-transitions.js +64 -0
  19. package/dist/backlog-transitions.js.map +1 -0
  20. package/dist/backlog.d.ts.map +1 -1
  21. package/dist/backlog.js +13 -0
  22. package/dist/backlog.js.map +1 -1
  23. package/dist/claim-check.d.ts.map +1 -1
  24. package/dist/claim-check.js +24 -2
  25. package/dist/claim-check.js.map +1 -1
  26. package/dist/claude-hooks-assets.d.ts +93 -0
  27. package/dist/claude-hooks-assets.d.ts.map +1 -0
  28. package/dist/claude-hooks-assets.js +208 -0
  29. package/dist/claude-hooks-assets.js.map +1 -0
  30. package/dist/clean-room-smoke.d.ts +83 -0
  31. package/dist/clean-room-smoke.d.ts.map +1 -0
  32. package/dist/clean-room-smoke.js +138 -0
  33. package/dist/clean-room-smoke.js.map +1 -0
  34. package/dist/cmd-usage.d.ts.map +1 -1
  35. package/dist/cmd-usage.js +36 -6
  36. package/dist/cmd-usage.js.map +1 -1
  37. package/dist/codex-hooks-assets.d.ts +25 -7
  38. package/dist/codex-hooks-assets.d.ts.map +1 -1
  39. package/dist/codex-hooks-assets.js +138 -30
  40. package/dist/codex-hooks-assets.js.map +1 -1
  41. package/dist/codex-hooks.d.ts +21 -1
  42. package/dist/codex-hooks.d.ts.map +1 -1
  43. package/dist/codex-hooks.js +21 -1
  44. package/dist/codex-hooks.js.map +1 -1
  45. package/dist/course-staleness.d.ts +19 -0
  46. package/dist/course-staleness.d.ts.map +1 -0
  47. package/dist/course-staleness.js +95 -0
  48. package/dist/course-staleness.js.map +1 -0
  49. package/dist/destructive-guard-hook.d.ts +40 -0
  50. package/dist/destructive-guard-hook.d.ts.map +1 -0
  51. package/dist/destructive-guard-hook.js +109 -0
  52. package/dist/destructive-guard-hook.js.map +1 -0
  53. package/dist/destructive-guard.d.ts +27 -0
  54. package/dist/destructive-guard.d.ts.map +1 -0
  55. package/dist/destructive-guard.js +2808 -0
  56. package/dist/destructive-guard.js.map +1 -0
  57. package/dist/discrimination-gate.d.ts +28 -3
  58. package/dist/discrimination-gate.d.ts.map +1 -1
  59. package/dist/discrimination-gate.js +76 -16
  60. package/dist/discrimination-gate.js.map +1 -1
  61. package/dist/feature-adr-routing.d.ts +95 -1
  62. package/dist/feature-adr-routing.d.ts.map +1 -1
  63. package/dist/feature-adr-routing.js +193 -27
  64. package/dist/feature-adr-routing.js.map +1 -1
  65. package/dist/guard.d.ts +90 -0
  66. package/dist/guard.d.ts.map +1 -1
  67. package/dist/guard.js +271 -0
  68. package/dist/guard.js.map +1 -1
  69. package/dist/harness-core-location.d.ts +18 -0
  70. package/dist/harness-core-location.d.ts.map +1 -0
  71. package/dist/harness-core-location.js +42 -0
  72. package/dist/harness-core-location.js.map +1 -0
  73. package/dist/index.d.ts +24 -6
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +24 -3
  76. package/dist/index.js.map +1 -1
  77. package/dist/lead-shift-gate.d.ts +95 -0
  78. package/dist/lead-shift-gate.d.ts.map +1 -0
  79. package/dist/lead-shift-gate.js +100 -0
  80. package/dist/lead-shift-gate.js.map +1 -0
  81. package/dist/ledger-backfill.d.ts +11 -1
  82. package/dist/ledger-backfill.d.ts.map +1 -1
  83. package/dist/ledger-backfill.js +19 -0
  84. package/dist/ledger-backfill.js.map +1 -1
  85. package/dist/loop-blobs.generated.d.ts +1 -1
  86. package/dist/loop-blobs.generated.d.ts.map +1 -1
  87. package/dist/loop-blobs.generated.js +12 -3
  88. package/dist/loop-blobs.generated.js.map +1 -1
  89. package/dist/loop-lint.d.ts.map +1 -1
  90. package/dist/loop-lint.js +56 -7
  91. package/dist/loop-lint.js.map +1 -1
  92. package/dist/loop-plan-graph.d.ts +1 -3
  93. package/dist/loop-plan-graph.d.ts.map +1 -1
  94. package/dist/loop-plan-graph.js +70 -1
  95. package/dist/loop-plan-graph.js.map +1 -1
  96. package/dist/loop-trace.d.ts.map +1 -1
  97. package/dist/loop-trace.js +16 -2
  98. package/dist/loop-trace.js.map +1 -1
  99. package/dist/managed-hooks.d.ts +5 -6
  100. package/dist/managed-hooks.d.ts.map +1 -1
  101. package/dist/managed-hooks.js +2 -2
  102. package/dist/managed-hooks.js.map +1 -1
  103. package/dist/model-recommender.d.ts.map +1 -1
  104. package/dist/model-recommender.js +14 -3
  105. package/dist/model-recommender.js.map +1 -1
  106. package/dist/operations.d.ts.map +1 -1
  107. package/dist/operations.js +82 -0
  108. package/dist/operations.js.map +1 -1
  109. package/dist/patterns.d.ts +23 -0
  110. package/dist/patterns.d.ts.map +1 -1
  111. package/dist/patterns.js +10 -0
  112. package/dist/patterns.js.map +1 -1
  113. package/dist/publish.d.ts +11 -0
  114. package/dist/publish.d.ts.map +1 -1
  115. package/dist/publish.js +16 -2
  116. package/dist/publish.js.map +1 -1
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +3 -2
  119. package/dist/registry.js.map +1 -1
  120. package/dist/score.d.ts.map +1 -1
  121. package/dist/score.js +15 -4
  122. package/dist/score.js.map +1 -1
  123. package/dist/session-retro.d.ts +121 -2
  124. package/dist/session-retro.d.ts.map +1 -1
  125. package/dist/session-retro.js +454 -11
  126. package/dist/session-retro.js.map +1 -1
  127. package/dist/setup.d.ts +28 -0
  128. package/dist/setup.d.ts.map +1 -1
  129. package/dist/setup.js +223 -6
  130. package/dist/setup.js.map +1 -1
  131. package/dist/sign.d.ts.map +1 -1
  132. package/dist/sign.js +18 -1
  133. package/dist/sign.js.map +1 -1
  134. package/dist/skills-verify.d.ts +34 -1
  135. package/dist/skills-verify.d.ts.map +1 -1
  136. package/dist/skills-verify.js +82 -0
  137. package/dist/skills-verify.js.map +1 -1
  138. package/dist/stage-line.d.ts +68 -0
  139. package/dist/stage-line.d.ts.map +1 -0
  140. package/dist/stage-line.js +129 -0
  141. package/dist/stage-line.js.map +1 -0
  142. package/dist/statusline.d.ts +99 -0
  143. package/dist/statusline.d.ts.map +1 -1
  144. package/dist/statusline.js +310 -44
  145. package/dist/statusline.js.map +1 -1
  146. package/dist/store-counts.d.ts +26 -0
  147. package/dist/store-counts.d.ts.map +1 -0
  148. package/dist/store-counts.js +125 -0
  149. package/dist/store-counts.js.map +1 -0
  150. package/dist/store-guard.d.ts +106 -0
  151. package/dist/store-guard.d.ts.map +1 -0
  152. package/dist/store-guard.js +294 -0
  153. package/dist/store-guard.js.map +1 -0
  154. package/dist/swarm-brief.d.ts +95 -0
  155. package/dist/swarm-brief.d.ts.map +1 -0
  156. package/dist/swarm-brief.js +660 -0
  157. package/dist/swarm-brief.js.map +1 -0
  158. package/dist/trace-bundle.d.ts +8 -0
  159. package/dist/trace-bundle.d.ts.map +1 -1
  160. package/dist/trace-bundle.js +11 -0
  161. package/dist/trace-bundle.js.map +1 -1
  162. package/package.json +12 -11
  163. package/sbom.json +804 -154
  164. package/src/agents-policy.ts +46 -2
  165. package/src/amendment-trace.ts +441 -19
  166. package/src/backlog-public.ts +503 -0
  167. package/src/backlog-transitions.ts +77 -0
  168. package/src/backlog.ts +12 -0
  169. package/src/claim-check.ts +25 -2
  170. package/src/claude-hooks-assets.ts +227 -0
  171. package/src/clean-room-smoke.ts +195 -0
  172. package/src/cmd-usage.ts +29 -5
  173. package/src/codex-hooks-assets.ts +140 -30
  174. package/src/codex-hooks.ts +21 -1
  175. package/src/course-staleness.ts +125 -0
  176. package/src/destructive-guard-hook.ts +151 -0
  177. package/src/destructive-guard.ts +3027 -0
  178. package/src/discrimination-gate.ts +98 -19
  179. package/src/feature-adr-routing.ts +220 -22
  180. package/src/guard.ts +318 -0
  181. package/src/harness-core-location.ts +44 -0
  182. package/src/index.ts +111 -3
  183. package/src/lead-shift-gate.ts +145 -0
  184. package/src/ledger-backfill.ts +20 -1
  185. package/src/loop-blobs.generated.ts +12 -3
  186. package/src/loop-lint.ts +52 -7
  187. package/src/loop-plan-graph.ts +66 -1
  188. package/src/loop-trace.ts +13 -1
  189. package/src/managed-hooks.ts +5 -6
  190. package/src/model-recommender.ts +14 -3
  191. package/src/operations.ts +75 -0
  192. package/src/patterns.ts +33 -0
  193. package/src/publish.ts +27 -2
  194. package/src/registry.ts +3 -2
  195. package/src/score.ts +16 -4
  196. package/src/session-retro.ts +466 -11
  197. package/src/setup.ts +246 -9
  198. package/src/sign.ts +18 -1
  199. package/src/skills-verify.ts +99 -1
  200. package/src/stage-line.ts +151 -0
  201. package/src/statusline.ts +396 -47
  202. package/src/store-counts.ts +154 -0
  203. package/src/store-guard.ts +388 -0
  204. package/src/swarm-brief.ts +661 -0
  205. package/src/trace-bundle.ts +10 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Публичный снимок бэклога: порождение, а не фильтрация.
3
+ *
4
+ * ЗАЧЕМ. Владелец хочет страницу «как строится харнесс в режиме реального времени». Наружу уходит
5
+ * производное от приватного стора, где каждая из 675 записей несёт полный текст задачи. Тексты не
6
+ * должны покидать машину никогда.
7
+ *
8
+ * ГЛАВНОЕ РЕШЕНИЕ (ADR-001), и оно объясняет форму всего файла: публичный объект НЕ ПРОВЕРЯЕТСЯ
9
+ * после сборки — он СОБИРАЕТСЯ здесь, перечислением разрешённых агрегатов, и приватное поле просто
10
+ * ни разу не читается на пути к выходу. Разница практическая: проверяющий поверх готового файла
11
+ * ловит то, о чём мы подумали, и пропускает поле, которое добавят завтра. Порождение не пропускает
12
+ * ничего, потому что не знает о существовании остального.
13
+ *
14
+ * ФУНКЦИЯ ЧИСТАЯ: два входа — записи и дата, — ни файловой системы, ни часов. Отсюда три свойства
15
+ * сразу: проверяется без файловой системы; одинаковый вход даёт байт-идентичный выход; и её нельзя
16
+ * сузить вызовом, потому что сужать нечем (INV-4).
17
+ *
18
+ * ИНВАРИАНТ, А НЕ ПЕРЕЧЕНЬ: наружу не уходит величина, ПРИВЯЗАННАЯ К ОДНОЙ ЗАПИСИ.
19
+ *
20
+ * Прежняя редакция перечисляла запрещённое — максимум, минимум, процентили — и была неполна:
21
+ * медиана при НЕЧЁТНОМ числе записей тоже есть значение ровно одной записи, а перечисление её не
22
+ * называло. Правило, сформулированное перечислением, всегда неполно, и неполнота читается как
23
+ * разрешение. Переформулировано 2026-09-03 решением владельца, пока в публичном ряду была ОДНА
24
+ * точка: после второй смена формулировки развела бы историю надвое.
25
+ *
26
+ * ЧЕСТНАЯ ГРАНИЦА: правило ОПИСЫВАЕТ, а не запрещает. Медиана при нечётном числе записей остаётся
27
+ * значением одной записи, и инвариант это честно покрывает — но не предотвращает. Исполнение
28
+ * машиной (отказ, если центральное значение разделяют менее MIN_GROUP записей) — отдельная работа,
29
+ * запись бэклога a72bb268.
30
+ *
31
+ * Максимум по определению есть значение РОВНО ОДНОЙ записи, то есть та же группа размера один,
32
+ * ради запрета которой написано правило малых групп. Состязательный рой нашёл это до реализации: 31,33 дня —
33
+ * тяжёлый выброс на фоне медианы в 2 дня, он переиздавался бы неизменным месяцами и стал бы
34
+ * устойчивым отпечатком одной идеи в каждом снимке. Вместо них — медиана и гистограмма по корзинам,
35
+ * и порог применяется к каждой корзине.
36
+ */
37
+ /** Версия схемы. Живёт В КАЖДОЙ точке ряда, а не только в корне: точка годичной давности должна
38
+ * читаться новым кодом без потерь (AM-6). */
39
+ export const PUBLIC_SCHEMA = 'backlog-public/1';
40
+ /** Порог малой группы. Два — минимум, при котором счётчик перестаёт быть указателем на одну запись. */
41
+ export const MIN_GROUP = 2;
42
+ /** Корзины времени до терминального состояния, в днях. Границы измерены, а не выбраны: на 288
43
+ * терминальных записях наполнение 120 · 62 · 84 · 3 · 19, корзин меньше порога ноль. */
44
+ export const TTT_BUCKETS = [
45
+ ['<1', 0, 1], ['1-3', 1, 3], ['3-7', 3, 7], ['7-14', 7, 14], ['14+', 14, Infinity],
46
+ ];
47
+ /** Терминальность — ЯВНЫЙ предикат, а не «есть отметка времени».
48
+ *
49
+ * До 2026-09-03 «есть statusTs» было тождественно «запись терминальна» — 288 из 288. Починка
50
+ * отметки времени в тот день эту тождественность разрушила: `roulette --commit` и `enrich` теперь
51
+ * тоже её ставят. Неявный фильтр по наличию отметки МОЛЧА включил бы записи в работе и испортил
52
+ * метрику в день собственного исправления. */
53
+ const TERMINAL = new Set(['shipped', 'dropped']);
54
+ const isTerminal = (r) => TERMINAL.has(r.status);
55
+ const KNOWN_STATUS = new Set(['new', 'enriched', 'in-progress', 'shipped', 'dropped']);
56
+ function daysBetween(a, b) {
57
+ const t0 = Date.parse(a), t1 = Date.parse(b);
58
+ if (!Number.isFinite(t0) || !Number.isFinite(t1))
59
+ return null;
60
+ const d = (t1 - t0) / 86_400_000;
61
+ return d >= 0 ? d : null;
62
+ }
63
+ /** Слить группы ниже порога в `other`; вернуть `null`, если и корзина оказалась мала. */
64
+ function collapseSmall(counts) {
65
+ const small = Object.entries(counts).filter(([, n]) => n < MIN_GROUP);
66
+ if (small.length === 0)
67
+ return { counts, skip: null };
68
+ const otherSum = small.reduce((s, [, n]) => s + n, 0);
69
+ if (otherSum < MIN_GROUP) {
70
+ return {
71
+ counts,
72
+ skip: `разрез пропущен: ${small.length} групп(ы) ниже порога ${MIN_GROUP}, их сумма ${otherSum} тоже ниже порога`,
73
+ };
74
+ }
75
+ const kept = {};
76
+ for (const [k, n] of Object.entries(counts))
77
+ if (n >= MIN_GROUP)
78
+ kept[k] = n;
79
+ kept.other = otherSum;
80
+ return { counts: kept, skip: null };
81
+ }
82
+ /**
83
+ * Построить публичный снимок.
84
+ *
85
+ * @param records разобранные приватные записи — читаются ТОЛЬКО поля `PrivateRecord`
86
+ * @param snapshotDate дата снимка, `YYYY-MM-DD`; ВХОД, а не обращение к часам (ADR-003)
87
+ */
88
+ export function buildPublicSnapshot(records, snapshotDate) {
89
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(snapshotDate)) {
90
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `дата снимка обязана быть YYYY-MM-DD, получено «${snapshotDate}»` };
91
+ }
92
+ const statusCounts = {};
93
+ const effortCounts = {};
94
+ const goalCounts = {};
95
+ const tttDays = [];
96
+ for (const r of records) {
97
+ if (!KNOWN_STATUS.has(r.status)) {
98
+ // Неизвестный статус — отказ, а не тихий пропуск: молчание превратило бы новое состояние
99
+ // в невидимое, и счётчики перестали бы сходиться с итогом без единого сообщения.
100
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `неизвестный статус «${r.status}» — схема разошлась с данными` };
101
+ }
102
+ statusCounts[r.status] = (statusCounts[r.status] ?? 0) + 1;
103
+ const e = String(r.effort ?? 'нет');
104
+ effortCounts[e] = (effortCounts[e] ?? 0) + 1;
105
+ const g = r.goalId ?? 'нет';
106
+ goalCounts[g] = (goalCounts[g] ?? 0) + 1;
107
+ // ЯВНЫЙ фильтр терминальности — см. комментарий к isTerminal.
108
+ if (isTerminal(r) && r.createdTs && r.statusTs) {
109
+ const d = daysBetween(r.createdTs, r.statusTs);
110
+ if (d !== null)
111
+ tttDays.push(d);
112
+ }
113
+ }
114
+ const effort = collapseSmall(effortCounts);
115
+ if (effort.skip !== null) {
116
+ return { ok: false, code: 'SMALL_GROUP', reason: `разрез по усилию: ${effort.skip}` };
117
+ }
118
+ const goals = collapseSmall(goalCounts);
119
+ // Гистограмма: порог применяется к КАЖДОЙ корзине, потому что корзина — такая же группа.
120
+ const buckets = {};
121
+ for (const [name, lo, hi] of TTT_BUCKETS)
122
+ buckets[name] = tttDays.filter((d) => d >= lo && d < hi).length;
123
+ const smallBucket = Object.entries(buckets).find(([, n]) => n > 0 && n < MIN_GROUP);
124
+ if (smallBucket) {
125
+ return {
126
+ ok: false, code: 'SMALL_GROUP',
127
+ reason: `корзина «${smallBucket[0]}» содержит ${smallBucket[1]} запись(и) — ниже порога ${MIN_GROUP}`,
128
+ };
129
+ }
130
+ const sorted = [...tttDays].sort((a, b) => a - b);
131
+ // Медиана по определению, а не «средний по счёту элемент». При ЧЁТНОМ числе значений средних
132
+ // два, и медиана — их полусумма; `sorted[floor(n/2)]` брал верхний из них.
133
+ //
134
+ // Это не педантизм, у ошибки была цена в приватности. Верхний центральный элемент — значение
135
+ // РОВНО ОДНОЙ записи, то есть та же группа размера один, ради запрета которой написано правило
136
+ // малых групп (см. шапку). Полусумма двух РАЗНЫХ центральных значений значением ни одной записи
137
+ // не является — и это единственная форма, в которой правка что-то даёт приватности. Оговорка
138
+ // несущая: когда два центральных значения СОВПАДАЮТ, полусумма равна им же, и медиана снова есть
139
+ // значение записей (тест A3 показывает ровно это). Найдено кросс-семейным ревью 2026-09-03:
140
+ // прежняя редакция комментария обещала больше, чем делает код.
141
+ // ИЗМЕРЕНО 2026-09-03 на живом сторе: n = 304, центральные 1,9879 и 2,0147, публиковалось 2,01
142
+ // (значение одной записи), истинная медиана — 2,00.
143
+ //
144
+ // ЧЕСТНАЯ ГРАНИЦА, чтобы её не пришлось искать заново: при НЕЧЁТНОМ n медиана по-прежнему есть
145
+ // значение одной записи — это свойство самого определения, а не дефект.
146
+ //
147
+ // СТРОКА КВИТАНЦИИ ЭТО ТЕПЕРЬ ПОКРЫВАЕТ: 2026-09-03 она переформулирована инвариантом «наружу не
148
+ // уходит величина, привязанная к одной записи» вместо прежнего перечисления. Правка сделана в тот
149
+ // же день сознательно — в публичном ряду была ОДНА точка, и после второй смена формулировки
150
+ // развела бы историю надвое.
151
+ //
152
+ // НО ПОКРЫВАЕТ — НЕ ЗНАЧИТ ЗАПРЕЩАЕТ. Правило описывает, машина его не исполняет: медиана при
153
+ // нечётном n уходит наружу как была. Заслон малой группы на саму медиану — запись a72bb268.
154
+ const median = sorted.length === 0
155
+ ? null
156
+ : Number(((sorted.length % 2 === 1
157
+ ? sorted[(sorted.length - 1) / 2]
158
+ : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2)).toFixed(2));
159
+ const snapshot = {
160
+ schema: PUBLIC_SCHEMA,
161
+ snapshotDate,
162
+ totals: { records: records.length, terminal: records.filter(isTerminal).length },
163
+ statusCounts,
164
+ effortCounts: effort.counts,
165
+ categoryCounts: goals.skip === null ? goals.counts : null,
166
+ categorySkipReason: goals.skip,
167
+ ttt: { medianDays: median, buckets },
168
+ receipt: {
169
+ checkedFields: Object.keys(statusCounts).length + Object.keys(effort.counts).length
170
+ + (goals.skip === null ? Object.keys(goals.counts).length : 0) + Object.keys(buckets).length,
171
+ checkedGroups: Object.keys(effort.counts).length + Object.keys(goals.counts).length + Object.keys(buckets).length,
172
+ appliedRules: [
173
+ `MIN_GROUP=${MIN_GROUP}`,
174
+ 'терминальность по статусу, не по наличию отметки',
175
+ 'наружу не уходит величина, привязанная к одной записи',
176
+ ...(goals.skip !== null ? ['разрез по целям пропущен'] : []),
177
+ ],
178
+ },
179
+ };
180
+ return { ok: true, snapshot };
181
+ }
182
+ /* ================================================================== */
183
+ /* ЗАСТАВА НА ВЫХОДЕ (ADR-004) — независимая проверка финальных байтов */
184
+ /* ================================================================== */
185
+ /**
186
+ * Разобрать то, что СЕЙЧАС уйдёт наружу, и отказать, если что-то не так.
187
+ *
188
+ * ЗАЧЕМ ОТДЕЛЬНАЯ ФУНКЦИЯ, если выше уже есть порождение. Порождение защищает СВОЙ выход. Как
189
+ * только появится второй путь наружу — а он появляется всегда, — эта гарантия его не покроет.
190
+ * Прецедент сработал 2026-08-30: сессионный хук засеял приватный ключ в каталог сборки за секунды
191
+ * до пуша, и починкой стала проверка ПО СОДЕРЖИМОМУ выходного дерева, а не доверие сборщику.
192
+ *
193
+ * ПОЧЕМУ СВОЙ ПЕРЕЧЕНЬ, А НЕ ИМПОРТ СХЕМЫ. Если бы застава брала перечень у генератора, одна
194
+ * ошибка проходила бы обе проверки сразу. Копия — сознательное дублирование; их расхождение
195
+ * ловится тестом и потому становится громким, а не тихим.
196
+ *
197
+ * ПЕРЕСЕЧЬ ЕЁ НЕЛЬЗЯ: один аргумент, никаких флагов. Проверка, которую вызывающий может ослабить,
198
+ * проверкой не является.
199
+ */
200
+ /** Собственная копия перечня. НЕ импортируется из схемы выше — см. комментарий. */
201
+ const EGRESS_ALLOWED_KEYS = [
202
+ 'schema', 'snapshotDate', 'totals', 'statusCounts', 'effortCounts',
203
+ 'categoryCounts', 'categorySkipReason', 'ttt', 'receipt',
204
+ ];
205
+ export function assertPublicSafe(bytes) {
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(bytes);
209
+ }
210
+ catch {
211
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: 'финальные байты не разбираются как JSON' };
212
+ }
213
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
214
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: 'публичный снимок обязан быть объектом' };
215
+ }
216
+ const obj = parsed;
217
+ // ОБЯЗАТЕЛЬНЫЕ ПОЛЯ: пустой объект — НЕ чистый снимок.
218
+ //
219
+ // Найдено кросс-семейным ревью (Codex gpt-5.6-sol, вердикт F, 2026-09-03): первая редакция
220
+ // возвращала `{ok:true, checkedKeys:0}` на `'{}'`. Это ровно та форма, против которой написан
221
+ // весь модуль: проверка выводила «чисто» из ОТСУТСТВИЯ данных, а не из их проверки.
222
+ for (const required of EGRESS_ALLOWED_KEYS) {
223
+ if (!(required in obj)) {
224
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `обязательное поле «${required}» отсутствует — неполный снимок не считается чистым` };
225
+ }
226
+ }
227
+ // РЕКУРСИВНЫЙ обход, а не только верхний уровень.
228
+ //
229
+ // Та же проверка (вердикт F) воспроизвела утечку: `{"receipt":{"text":"ПРИВАТНЫЙ ТЕКСТ"}}`
230
+ // проходила, потому что застава смотрела ТОЛЬКО верхние ключи, а `receipt` в перечне разрешён.
231
+ // Дыра существовала в каждом разрешённом поле — ttt, totals, любом. Белый список верхнего уровня
232
+ // не является белым списком: он проверяет имена дверей, не заглядывая в комнаты.
233
+ // Перечень покрывает ВСЁ ДЕРЕВО, а не только корень. Первая починка проверяла лишь ФОРМУ
234
+ // вложенного имени («похоже на техническое») — и короткий приватный текст под именем `text`
235
+ // проезжал: имя техничное, строка короче порога. Форма имени ничего не гарантирует; гарантирует
236
+ // только перечень. Ключи-ДАННЫЕ (имена статусов, целей, корзин) перечислить нельзя — они
237
+ // приходят из данных, — поэтому для них поле объявлено словарём, и проверяется ЗНАЧЕНИЕ: число.
238
+ const NESTED = {
239
+ totals: ['records', 'terminal'],
240
+ ttt: ['medianDays', 'buckets'],
241
+ 'ttt.buckets': TTT_BUCKETS.map(([n]) => n),
242
+ receipt: ['checkedFields', 'checkedGroups', 'appliedRules'],
243
+ statusCounts: ['new', 'enriched', 'in-progress', 'shipped', 'dropped'],
244
+ effortCounts: ['1', '2', '3', '4', '5', 'нет', 'other'],
245
+ // ЕДИНСТВЕННЫЙ словарь с неперечислимыми ключами: имена целей приходят из данных владельца и
246
+ // заранее неизвестны. Третий проход ревью показал, почему это опасно: ИМЯ КЛЮЧА само есть
247
+ // канал — `{"statusCounts":{"PRIVATE_TASK_TEXT":2}}` проходил, потому что проверялись только
248
+ // форма имени и тип значения. Статусы, усилия и корзины перечислимы, и теперь перечислены.
249
+ // Для целей форма имени — единственная защита, и она названа слабой прямо здесь.
250
+ categoryCounts: 'dictionary-of-numbers',
251
+ };
252
+ const DICT_KEY_RE = /^[A-Za-z0-9_<>+.-]{1,60}$/;
253
+ // СВОБОДНАЯ СТРОКА — ЭТО КАНАЛ, даже короткая и даже в разрешённом поле.
254
+ //
255
+ // Второй канал, названный тем же ревью: `receipt.appliedRules` — массив строк, и порог длины в
256
+ // 200 символов пропускал приватный текст, если он короче. Порог ловит ПРОЗУ, но не ловит
257
+ // короткое сообщение, а утечке достаточно короткого. Поэтому строковые значения проверяются не
258
+ // длиной, а ПРИНАДЛЕЖНОСТЬЮ: каждое из них либо перечислено, либо имеет машинную форму.
259
+ const ALLOWED_RULE = /^(MIN_GROUP=\d+|терминальность по статусу, не по наличию отметки|наружу не уходит величина, привязанная к одной записи|разрез по целям пропущен)$/;
260
+ const ALLOWED_STRING_AT = {
261
+ schema: /^backlog-public\/1$/,
262
+ snapshotDate: /^\d{4}-\d{2}-\d{2}$/,
263
+ categorySkipReason: /^разрез пропущен: \d+ групп\(ы\) ниже порога \d+, их сумма \d+ тоже ниже порога$/,
264
+ 'receipt.appliedRules': ALLOWED_RULE,
265
+ };
266
+ const walk = (node, at, depth) => {
267
+ if (depth > 6)
268
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `слишком глубокая вложенность в ${at}` };
269
+ if (node === null || typeof node === 'number' || typeof node === 'boolean')
270
+ return null;
271
+ if (typeof node === 'string') {
272
+ // Место строки определяет, что ей разрешено. Строка там, где строк не ждут, — отказ; строка
273
+ // не той формы там, где ждут, — тоже отказ. Длина остаётся вторым рубежом.
274
+ const base = at.replace(/\[\d+\]$/, '');
275
+ const pattern = ALLOWED_STRING_AT[base];
276
+ if (pattern === undefined) {
277
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `строка в ${at} — это поле не объявлено строковым, значит не публикуется` };
278
+ }
279
+ if (!pattern.test(node)) {
280
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `строка в ${at} не соответствует объявленной форме: «${node.slice(0, 60)}»` };
281
+ }
282
+ if (node.length > 200) {
283
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `строка в ${at} длиной ${node.length} — публичный снимок не носит прозу` };
284
+ }
285
+ return null;
286
+ }
287
+ if (Array.isArray(node)) {
288
+ for (let i = 0; i < node.length; i += 1) {
289
+ const bad = walk(node[i], `${at}[${i}]`, depth + 1);
290
+ if (bad)
291
+ return bad;
292
+ }
293
+ return null;
294
+ }
295
+ if (typeof node === 'object') {
296
+ for (const [k, v] of Object.entries(node)) {
297
+ if (at === '' && !EGRESS_ALLOWED_KEYS.includes(k)) {
298
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `ключ «${k}» отсутствует в перечне разрешённых` };
299
+ }
300
+ if (at !== '') {
301
+ const rule = NESTED[at];
302
+ if (rule === undefined) {
303
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `поле ${at} не объявлено в перечне вложенных — его содержимое не проверяется, значит не публикуется` };
304
+ }
305
+ if (rule === 'dictionary-of-numbers') {
306
+ if (!DICT_KEY_RE.test(k)) {
307
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `ключ словаря «${k}» в ${at} не похож на имя группы` };
308
+ }
309
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
310
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `${at}.${k} — словарь счётчиков не носит ничего, кроме чисел` };
311
+ }
312
+ }
313
+ else if (!rule.includes(k)) {
314
+ return { ok: false, code: 'FORBIDDEN_FIELD', reason: `вложенный ключ «${k}» в ${at} отсутствует в перечне разрешённых` };
315
+ }
316
+ }
317
+ const bad = walk(v, at === '' ? k : `${at}.${k}`, depth + 1);
318
+ if (bad)
319
+ return bad;
320
+ }
321
+ return null;
322
+ }
323
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `неожиданный тип в ${at}` };
324
+ };
325
+ const deep = walk(obj, '', 0);
326
+ if (deep)
327
+ return deep;
328
+ // ПРОВЕРЯТЬ НАДО БАЙТЫ, А НЕ РАЗОБРАННЫЙ ОБЪЕКТ — и это второй урок того же ревью.
329
+ //
330
+ // Найдено кросс-семейным ревью (вердикт F, второй проход): вход с ДУБЛИРУЮЩИМ ключом
331
+ // `{"receipt":{"text":"ПРИВАТНЫЙ ТЕКСТ"},"receipt":{...}}` проходил. JSON.parse оставляет
332
+ // ПОСЛЕДНИЙ `receipt`, обход видит чистый объект и говорит «чисто» — а наружу уезжают ОБА,
333
+ // потому что наружу уезжает строка, а не объект.
334
+ //
335
+ // Общая форма ошибки: проверяющий смотрел не на тот объект. Наружу уходят БАЙТЫ, значит
336
+ // проверять надо байты. Способ: сериализовать разобранное обратно и сверить с исходным — всё,
337
+ // что разбор проглотил (дубликаты ключей, лишние пробелы, комментарии), даст расхождение.
338
+ const canonical = JSON.stringify(obj);
339
+ if (canonical !== bytes.trim()) {
340
+ return {
341
+ ok: false,
342
+ code: 'SCHEMA_DRIFT',
343
+ reason: 'байты не совпадают с их же разбором — в них есть то, чего разбор не видит '
344
+ + `(дубликат ключа, лишнее форматирование): ${bytes.trim().length} байт против ${canonical.length}`,
345
+ };
346
+ }
347
+ // Каждая опубликованная группа не ниже порога. Проверяется ПО СОДЕРЖИМОМУ, а не по вере в то,
348
+ // что генератор уже посчитал.
349
+ let groups = 0;
350
+ for (const field of ['statusCounts', 'effortCounts', 'categoryCounts']) {
351
+ const v = obj[field];
352
+ if (v === null || v === undefined)
353
+ continue;
354
+ if (typeof v !== 'object') {
355
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `поле ${field} обязано быть объектом счётчиков` };
356
+ }
357
+ for (const [k, n] of Object.entries(v)) {
358
+ if (typeof n !== 'number' || !Number.isFinite(n)) {
359
+ return { ok: false, code: 'SCHEMA_DRIFT', reason: `счётчик ${field}.${k} не число` };
360
+ }
361
+ if (n > 0 && n < MIN_GROUP) {
362
+ return { ok: false, code: 'SMALL_GROUP', reason: `${field}.${k} = ${n} — ниже порога ${MIN_GROUP}` };
363
+ }
364
+ groups += 1;
365
+ }
366
+ }
367
+ // Производная малая группа: если итог минус сумма опубликованных даёт остаток ниже порога, то
368
+ // этот остаток восстанавливается ВЫЧИТАНИЕМ, и публиковать его нельзя даже неявно.
369
+ const totals = obj.totals;
370
+ const total = typeof totals?.records === 'number' ? totals.records : null;
371
+ if (total !== null) {
372
+ for (const field of ['statusCounts', 'effortCounts']) {
373
+ const v = obj[field];
374
+ if (!v)
375
+ continue;
376
+ const sum = Object.values(v).reduce((s, n) => s + n, 0);
377
+ const remainder = total - sum;
378
+ if (remainder > 0 && remainder < MIN_GROUP) {
379
+ return {
380
+ ok: false, code: 'DERIVABLE_SMALL_GROUP',
381
+ reason: `${field}: итог ${total} минус сумма ${sum} даёт ${remainder} — группа восстанавливается вычитанием`,
382
+ };
383
+ }
384
+ }
385
+ }
386
+ return { ok: true, checkedKeys: Object.keys(obj).length, checkedGroups: groups };
387
+ }
388
+ /**
389
+ * Потолок отсрочки в днях. Три — минимум, при котором ОКНО становится четырёхдневным и движение
390
+ * перестаёт указывать на один день. НЕ «минимум, при котором разность больше единицы»: так было
391
+ * написано до 2026-09-03 и это опровергнуто измерением (6 из 6 агрегатов несли разность ровно 1).
392
+ */
393
+ export const HOLD_CAP_DAYS = 3;
394
+ /**
395
+ * @param previous значение столбца в ПОСЛЕДНЕМ опубликованном снимке
396
+ * @param current значение сейчас
397
+ * @param heldDays сколько дней этот столбец уже отложен
398
+ */
399
+ export function applyDeltaRule(previous, current, heldDays) {
400
+ if (previous === null)
401
+ return { action: 'publish', value: current };
402
+ const delta = Math.abs(current - previous);
403
+ if (delta !== 1)
404
+ return { action: 'publish', value: current };
405
+ if (heldDays >= HOLD_CAP_DAYS) {
406
+ return {
407
+ action: 'publish-aggregated', value: current, overPeriodDays: heldDays + 1,
408
+ };
409
+ }
410
+ return {
411
+ action: 'hold', heldDays: heldDays + 1,
412
+ reason: `разность ровно 1 — столбец отложен, чтобы движение не указывало на одну запись (отсрочка ${heldDays + 1} из ${HOLD_CAP_DAYS})`,
413
+ };
414
+ }
415
+ //# sourceMappingURL=backlog-public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlog-public.js","sourceRoot":"","sources":["../src/backlog-public.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Журнал переходов статуса — append-only, по строке на переход.
3
+ *
4
+ * ЗАЧЕМ. Запись бэклога хранит РОВНО ОДИН переход: поле `statusTs` перезаписывается на каждом
5
+ * следующем. Промежуточные переходы, повторные открытия и время, проведённое в каждом статусе,
6
+ * стираются по построению. Измерено 2026-09-02: из 663 записей отметку несут 288, и все 288 —
7
+ * терминальные; «сколько идея пробыла в работе» невычислимо ни для одной.
8
+ *
9
+ * ПОЧЕМУ ЭТО СРОЧНО, а не «когда дойдут руки». Прошлое невосстановимо: каждый день без журнала
10
+ * навсегда вычитается из будущей аналитики. Панель можно построить когда угодно — данные для неё
11
+ * набираются только с того дня, когда начали писать.
12
+ *
13
+ * ЧЕГО ЭТОТ ФАЙЛ НЕ ДЕЛАЕТ. Он не восстанавливает историю задним числом и не пытается: 12 случайных
14
+ * снимков дают 99 переходов, но снимки нерегулярны и делались при СЛИЯНИИ, то есть в моменты,
15
+ * коррелирующие с активностью. Восстановленное из них — отдельный слой с собственной пометкой, а
16
+ * не строки этого журнала.
17
+ *
18
+ * ФОРМАТ. JSONL: одна строка — один переход, дозапись без чтения всего файла. Порча одной строки
19
+ * не делает нечитаемым остальное — ровно поэтому не JSON-массив.
20
+ */
21
+ /** Одна строка журнала: кто, откуда, куда, когда и по какой причине. */
22
+ export interface TransitionRow {
23
+ /** Идентификатор записи бэклога. */
24
+ readonly id: string;
25
+ /** Статус ДО перехода. `null` — запись только что создана. */
26
+ readonly from: string | null;
27
+ readonly to: string;
28
+ /** ISO-время. Передаётся ВЫЗЫВАЮЩИМ, а не берётся из часов: иначе функция нечистая и невоспроизводима. */
29
+ readonly ts: string;
30
+ /** Причина, если её назвали. Свободный текст: здесь он уместен, это летопись, а не гейт. */
31
+ readonly reason?: string | undefined;
32
+ /** Чем вызван переход: имя команды. Отвечает на вопрос «кто это сделал». */
33
+ readonly by?: string | undefined;
34
+ }
35
+ /** Путь журнала. Рядом с самим бэклогом, чтобы переезд стора не разлучил их. */
36
+ export declare function transitionLogPath(projectRoot: string): string;
37
+ /**
38
+ * Дозаписать переход. НИКОГДА не бросает: журнал — наблюдение, а не гейт, и его поломка не должна
39
+ * ронять команду, которая меняла статус. Возвращает признак успеха, чтобы вызывающий мог сказать
40
+ * вслух, если запись не легла — молчаливая потеря наблюдения хуже громкой.
41
+ */
42
+ export declare function appendTransition(projectRoot: string, row: TransitionRow): boolean;
43
+ /**
44
+ * Прочитать журнал. Битая строка ПРОПУСКАЕТСЯ, а не роняет чтение: файл дозаписывается конкурентно,
45
+ * и оборванная последняя строка — нормальное состояние, а не повод потерять всю летопись.
46
+ */
47
+ export declare function readTransitions(projectRoot: string): readonly TransitionRow[];
48
+ //# sourceMappingURL=backlog-transitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlog-transitions.d.ts","sourceRoot":"","sources":["../src/backlog-transitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CASjF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,aAAa,EAAE,CAY7E"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Журнал переходов статуса — append-only, по строке на переход.
3
+ *
4
+ * ЗАЧЕМ. Запись бэклога хранит РОВНО ОДИН переход: поле `statusTs` перезаписывается на каждом
5
+ * следующем. Промежуточные переходы, повторные открытия и время, проведённое в каждом статусе,
6
+ * стираются по построению. Измерено 2026-09-02: из 663 записей отметку несут 288, и все 288 —
7
+ * терминальные; «сколько идея пробыла в работе» невычислимо ни для одной.
8
+ *
9
+ * ПОЧЕМУ ЭТО СРОЧНО, а не «когда дойдут руки». Прошлое невосстановимо: каждый день без журнала
10
+ * навсегда вычитается из будущей аналитики. Панель можно построить когда угодно — данные для неё
11
+ * набираются только с того дня, когда начали писать.
12
+ *
13
+ * ЧЕГО ЭТОТ ФАЙЛ НЕ ДЕЛАЕТ. Он не восстанавливает историю задним числом и не пытается: 12 случайных
14
+ * снимков дают 99 переходов, но снимки нерегулярны и делались при СЛИЯНИИ, то есть в моменты,
15
+ * коррелирующие с активностью. Восстановленное из них — отдельный слой с собственной пометкой, а
16
+ * не строки этого журнала.
17
+ *
18
+ * ФОРМАТ. JSONL: одна строка — один переход, дозапись без чтения всего файла. Порча одной строки
19
+ * не делает нечитаемым остальное — ровно поэтому не JSON-массив.
20
+ */
21
+ import { appendFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+ /** Путь журнала. Рядом с самим бэклогом, чтобы переезд стора не разлучил их. */
24
+ export function transitionLogPath(projectRoot) {
25
+ return join(projectRoot, '.dz', 'backlog', 'status-log.jsonl');
26
+ }
27
+ /**
28
+ * Дозаписать переход. НИКОГДА не бросает: журнал — наблюдение, а не гейт, и его поломка не должна
29
+ * ронять команду, которая меняла статус. Возвращает признак успеха, чтобы вызывающий мог сказать
30
+ * вслух, если запись не легла — молчаливая потеря наблюдения хуже громкой.
31
+ */
32
+ export function appendTransition(projectRoot, row) {
33
+ try {
34
+ const p = transitionLogPath(projectRoot);
35
+ mkdirSync(dirname(p), { recursive: true });
36
+ appendFileSync(p, `${JSON.stringify(row)}\n`, 'utf8');
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * Прочитать журнал. Битая строка ПРОПУСКАЕТСЯ, а не роняет чтение: файл дозаписывается конкурентно,
45
+ * и оборванная последняя строка — нормальное состояние, а не повод потерять всю летопись.
46
+ */
47
+ export function readTransitions(projectRoot) {
48
+ const p = transitionLogPath(projectRoot);
49
+ if (!existsSync(p))
50
+ return [];
51
+ const out = [];
52
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
53
+ if (!line.trim())
54
+ continue;
55
+ try {
56
+ const r = JSON.parse(line);
57
+ if (typeof r?.id === 'string' && typeof r?.to === 'string' && typeof r?.ts === 'string')
58
+ out.push(r);
59
+ }
60
+ catch { /* битая строка пропускается */ }
61
+ }
62
+ return out;
63
+ }
64
+ //# sourceMappingURL=backlog-transitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlog-transitions.js","sourceRoot":"","sources":["../src/backlog-transitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiB1C,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,GAAkB;IACtE,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;QACzC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,cAAc,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB;IACjD,MAAM,CAAC,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;YAC5C,IAAI,OAAO,CAAC,EAAE,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,EAAE,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,EAAE,EAAE,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvG,CAAC;QAAC,MAAM,CAAC,CAAC,+BAA+B,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}