usedesign 0.3.0 → 0.5.0
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/checks.d.ts +2 -0
- package/dist/checks.js +0 -0
- package/dist/checks.js.map +1 -1
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +160 -23
- package/dist/cli.js.map +1 -1
- package/dist/preview.d.ts +16 -0
- package/dist/preview.js +334 -0
- package/dist/preview.js.map +1 -0
- package/dist/scaffold.d.ts +35 -0
- package/dist/scaffold.js +242 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema/form-contract.schema.json +82 -0
- package/dist/schema/form-inventory.schema.json +37 -0
- package/dist/schema/operation-card.schema.json +23 -0
- package/dist/schema/storage-inventory.schema.json +50 -0
- package/dist/validate.js +35 -3
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
package/dist/preview.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `usedesign preview` — a rough, honest visualiser of form contracts.
|
|
3
|
+
*
|
|
4
|
+
* Generates ONE self-contained HTML file: no server, no dependencies, openable from disk. Three
|
|
5
|
+
* rails — the contract tree, the contract's content, and a grey-box wireframe of the selected
|
|
6
|
+
* form in a selected entity state. When a form inventory is present, every wireframe element
|
|
7
|
+
* carries a verdict badge: rendered, missing (the contract is ahead of the code), or rendered
|
|
8
|
+
* beyond the contract.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately crude: grey boxes and real labels. A pretty preview would start competing with
|
|
11
|
+
* the product's own design system and lose; the wireframe's job is to show WHAT the owner
|
|
12
|
+
* decided, state by state, and where reality disagrees.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { frontMatter, expandGlob } from "./core.js";
|
|
17
|
+
function bodyOf(path) {
|
|
18
|
+
const text = readFileSync(path, "utf8").replace(/^/, "").replace(/\r\n/g, "\n");
|
|
19
|
+
const end = text.indexOf("\n---", 3);
|
|
20
|
+
return end === -1 ? "" : text.slice(end + 4).trim();
|
|
21
|
+
}
|
|
22
|
+
export function collectPreviewData(config, base) {
|
|
23
|
+
const contracts = [];
|
|
24
|
+
for (const pattern of config.forms ?? []) {
|
|
25
|
+
for (const file of expandGlob(base, pattern)) {
|
|
26
|
+
const fm = frontMatter(file);
|
|
27
|
+
if (fm && fm["usedesign_form"] === 1)
|
|
28
|
+
contracts.push({ ...fm, __body: bodyOf(file), __file: file.split(/[\\/]/).pop() });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let inventory = null;
|
|
32
|
+
let producedBy = "";
|
|
33
|
+
const location = config.form_inventory;
|
|
34
|
+
if (location && existsSync(join(base, location))) {
|
|
35
|
+
const data = JSON.parse(readFileSync(join(base, location), "utf8"));
|
|
36
|
+
producedBy = data.produced_by ?? "";
|
|
37
|
+
inventory = {};
|
|
38
|
+
for (const form of data.forms ?? []) {
|
|
39
|
+
const states = {};
|
|
40
|
+
for (const s of form.states ?? [])
|
|
41
|
+
states[s.state] = { fields: s.fields ?? [], controls: s.controls ?? [] };
|
|
42
|
+
inventory[form.screen] = states;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { contracts, inventory, producedBy };
|
|
46
|
+
}
|
|
47
|
+
export function renderPreviewHtml(data) {
|
|
48
|
+
const payload = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
49
|
+
return `<!doctype html>
|
|
50
|
+
<html lang="ru">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
54
|
+
<title>usedesign — контракты форм</title>
|
|
55
|
+
<style>
|
|
56
|
+
:root{--bg:#1114;--line:#d8d8d2;--ink:#22251f;--dim:#7a7d74;--ok:#2c7a3f;--todo:#b3372b;--extra:#8a6d1a;--card:#fbfbf8;}
|
|
57
|
+
*{box-sizing:border-box}
|
|
58
|
+
body{margin:0;font:14px/1.45 ui-sans-serif,system-ui,'Segoe UI',sans-serif;color:var(--ink);background:#efefe9;height:100vh;display:flex;flex-direction:column}
|
|
59
|
+
header{padding:10px 16px;border-bottom:1px solid var(--line);background:var(--card);display:flex;gap:12px;align-items:baseline}
|
|
60
|
+
header h1{font-size:15px;margin:0}
|
|
61
|
+
header .sub{color:var(--dim);font-size:12px}
|
|
62
|
+
/* лента рейлов: горизонтальная, без переноса, каждый рейл — своя ширина и свой скролл */
|
|
63
|
+
.strip{flex:1;display:flex;overflow-x:auto;overflow-y:hidden;align-items:stretch;gap:0;justify-content:flex-start}
|
|
64
|
+
.rail{flex:0 0 auto;display:flex;flex-direction:column;min-width:0;background:var(--card);border-right:1px solid var(--line);position:relative}
|
|
65
|
+
.rail>.body{flex:1;overflow-y:auto;padding:12px 14px}
|
|
66
|
+
.rail h2{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin:0;padding:10px 14px 6px}
|
|
67
|
+
.grip{position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;z-index:5}
|
|
68
|
+
.grip:hover{background:#0002}
|
|
69
|
+
/* рейл 1: дерево */
|
|
70
|
+
.tree a{display:block;padding:5px 8px;border-radius:6px;color:inherit;text-decoration:none}
|
|
71
|
+
.tree a:hover{background:#0000000d}
|
|
72
|
+
.tree a.sel{background:#22251f;color:#fff}
|
|
73
|
+
.tree .page{color:var(--dim);font-size:11px;margin:10px 0 2px;text-transform:uppercase;letter-spacing:.06em}
|
|
74
|
+
.tree .opens{margin-left:16px;border-left:1px dotted var(--line);padding-left:8px}
|
|
75
|
+
/* рейл 2: содержание */
|
|
76
|
+
.kv{margin:0 0 12px}
|
|
77
|
+
.kv dt{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;margin-top:10px}
|
|
78
|
+
.kv dd{margin:2px 0 0}
|
|
79
|
+
.item{padding:7px 9px;border:1px solid var(--line);border-radius:8px;margin:6px 0;background:#fff}
|
|
80
|
+
.item .name{font-family:ui-monospace,monospace;font-size:12px}
|
|
81
|
+
.item .shows{color:var(--ink)}
|
|
82
|
+
.item .meta{color:var(--dim);font-size:12px}
|
|
83
|
+
.prose{white-space:pre-wrap;color:var(--dim);font-size:12.5px;border-top:1px dashed var(--line);padding-top:10px;margin-top:14px}
|
|
84
|
+
/* рейл 3: каркас */
|
|
85
|
+
.states{display:flex;gap:6px;flex-wrap:wrap;padding:0 14px 8px}
|
|
86
|
+
.states button{border:1px solid var(--line);background:#fff;border-radius:999px;padding:3px 10px;cursor:pointer;font:inherit;font-size:12px}
|
|
87
|
+
.states button.sel{background:#22251f;color:#fff;border-color:#22251f}
|
|
88
|
+
.wire{border:1px solid var(--line);border-radius:12px;background:#fff;padding:10px;display:flex;flex-direction:column;gap:8px}
|
|
89
|
+
.el{border:1px dashed #b9b9b0;border-radius:8px;padding:8px 10px;background:#f6f6f1}
|
|
90
|
+
.el .name{font-family:ui-monospace,monospace;font-size:11px;color:var(--dim)}
|
|
91
|
+
.el .shows{font-size:13px}
|
|
92
|
+
.btnrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
|
93
|
+
.ctl{border:1px solid #22251f;border-radius:8px;padding:6px 12px;background:#fff;font-size:13px}
|
|
94
|
+
.ctl.rule{border-style:dashed;color:var(--dim)}
|
|
95
|
+
.ctl.removedghost{border-style:dotted;color:var(--todo);text-decoration:line-through}
|
|
96
|
+
.badge{display:inline-block;font-size:10.5px;border-radius:999px;padding:1px 7px;margin-left:6px;vertical-align:1px}
|
|
97
|
+
.b-ok{background:#e3f2e6;color:var(--ok)}
|
|
98
|
+
.b-todo{background:#fbe4e0;color:var(--todo)}
|
|
99
|
+
.b-extra{background:#f7eecb;color:var(--extra)}
|
|
100
|
+
.legend{color:var(--dim);font-size:11.5px;padding:8px 14px;border-top:1px solid var(--line)}
|
|
101
|
+
.extras{margin-top:12px}
|
|
102
|
+
.extras .el{border-style:dotted;background:#fdf9ea}
|
|
103
|
+
@media (max-width: 900px){ .strip{flex-direction:column;overflow:visible} .rail{width:auto !important;flex:0 0 auto;border-right:0;border-bottom:1px solid var(--line)} .grip{display:none} }
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body data-page="usedesign-preview">
|
|
107
|
+
<header data-panel="preview-header">
|
|
108
|
+
<h1>usedesign · контракты форм</h1>
|
|
109
|
+
<span class="sub" id="src"></span>
|
|
110
|
+
</header>
|
|
111
|
+
<div class="strip" id="strip" data-panel="rail-strip">
|
|
112
|
+
<section class="rail" id="rail-tree" data-panel="contract-tree" style="width:260px">
|
|
113
|
+
<h2>Контракты</h2>
|
|
114
|
+
<div class="body tree" id="tree" data-list="contracts"></div>
|
|
115
|
+
<div class="grip" data-control="resize-tree"></div>
|
|
116
|
+
</section>
|
|
117
|
+
<section class="rail" id="rail-doc" data-panel="contract-doc" style="width:420px">
|
|
118
|
+
<h2>Содержание</h2>
|
|
119
|
+
<div class="body" id="doc" data-group="contract-content"></div>
|
|
120
|
+
<div class="grip" data-control="resize-doc"></div>
|
|
121
|
+
</section>
|
|
122
|
+
<section class="rail" id="rail-wire" data-panel="contract-wireframe" style="width:460px">
|
|
123
|
+
<h2>Каркас-превью</h2>
|
|
124
|
+
<div class="states" id="states" data-group="state-toggle"></div>
|
|
125
|
+
<div class="body" id="wire" data-group="wireframe"></div>
|
|
126
|
+
<div class="legend" data-group="legend">
|
|
127
|
+
<span class="badge b-ok">есть</span> отрендерено ·
|
|
128
|
+
<span class="badge b-todo">нет в коде</span> контракт впереди кода ·
|
|
129
|
+
<span class="badge b-extra">вне контракта</span> отрендерено, но не решено
|
|
130
|
+
</div>
|
|
131
|
+
<div class="grip" data-control="resize-wire"></div>
|
|
132
|
+
</section>
|
|
133
|
+
</div>
|
|
134
|
+
<script id="data" type="application/json">${payload}</script>
|
|
135
|
+
<script>
|
|
136
|
+
const DATA = JSON.parse(document.getElementById('data').textContent);
|
|
137
|
+
const $ = (id) => document.getElementById(id);
|
|
138
|
+
const params = () => new URLSearchParams(location.search);
|
|
139
|
+
|
|
140
|
+
// ── рейлы: ширины с ограничениями, перетаскивание, сохранение ────────────────────────────────
|
|
141
|
+
const RAILS = [
|
|
142
|
+
{ el: 'rail-tree', grip: 0, def: 260, min: 180, max: 420 },
|
|
143
|
+
{ el: 'rail-doc', grip: 1, def: 420, min: 280, max: 680 },
|
|
144
|
+
{ el: 'rail-wire', grip: 2, def: 460, min: 320, max: 760 },
|
|
145
|
+
];
|
|
146
|
+
for (const r of RAILS) {
|
|
147
|
+
const saved = Number(localStorage.getItem('ud.rail.' + r.el));
|
|
148
|
+
const w = Number.isFinite(saved) && saved >= r.min && saved <= r.max ? saved : r.def;
|
|
149
|
+
$(r.el).style.width = w + 'px';
|
|
150
|
+
const grip = $(r.el).querySelector('.grip');
|
|
151
|
+
grip.addEventListener('pointerdown', (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
const startX = e.clientX, startW = $(r.el).getBoundingClientRect().width;
|
|
154
|
+
const move = (ev) => {
|
|
155
|
+
const w2 = Math.min(r.max, Math.max(r.min, startW + ev.clientX - startX));
|
|
156
|
+
$(r.el).style.width = w2 + 'px';
|
|
157
|
+
};
|
|
158
|
+
const up = () => {
|
|
159
|
+
localStorage.setItem('ud.rail.' + r.el, String($(r.el).getBoundingClientRect().width | 0));
|
|
160
|
+
window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up);
|
|
161
|
+
};
|
|
162
|
+
window.addEventListener('pointermove', move); window.addEventListener('pointerup', up);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// колесо над «тихим» местом листает ленту; клик по подрезанному рейлу довозит его
|
|
166
|
+
$('strip').addEventListener('wheel', (e) => {
|
|
167
|
+
let node = e.target;
|
|
168
|
+
while (node && node !== document.body) {
|
|
169
|
+
if (node.scrollHeight > node.clientHeight + 1 && getComputedStyle(node).overflowY !== 'hidden') return;
|
|
170
|
+
node = node.parentElement;
|
|
171
|
+
}
|
|
172
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { $('strip').scrollLeft += e.deltaY; e.preventDefault(); }
|
|
173
|
+
}, { passive: false });
|
|
174
|
+
for (const r of RAILS) $(r.el).addEventListener('click', (e) => {
|
|
175
|
+
if (e.target.closest('.grip')) return;
|
|
176
|
+
const el = $(r.el), s = $('strip');
|
|
177
|
+
const left = el.offsetLeft - s.scrollLeft, right = left + el.offsetWidth;
|
|
178
|
+
if (left < 0 || right > s.clientWidth) el.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── выбор: источник истины — URL ─────────────────────────────────────────────────────────────
|
|
182
|
+
function select(id, state, push = true) {
|
|
183
|
+
const q = params();
|
|
184
|
+
q.set('form', id);
|
|
185
|
+
if (state) q.set('state', state); else q.delete('state');
|
|
186
|
+
if (push) history.pushState(null, '', '?' + q.toString());
|
|
187
|
+
renderAll();
|
|
188
|
+
}
|
|
189
|
+
window.addEventListener('popstate', renderAll);
|
|
190
|
+
|
|
191
|
+
function current() {
|
|
192
|
+
const q = params();
|
|
193
|
+
const id = q.get('form') || (DATA.contracts[0] && DATA.contracts[0].id);
|
|
194
|
+
const c = DATA.contracts.find((x) => x.id === id) || DATA.contracts[0];
|
|
195
|
+
return { c, state: q.get('state') };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function statesOf(c) {
|
|
199
|
+
const set = new Set();
|
|
200
|
+
const inv = DATA.inventory && DATA.inventory[c.screen];
|
|
201
|
+
if (inv) for (const s of Object.keys(inv)) set.add(s);
|
|
202
|
+
for (const p of c.presents || []) for (const s of p.when || []) set.add(s);
|
|
203
|
+
for (const k of c.controls || []) for (const s of k.shown_when || []) set.add(s);
|
|
204
|
+
return set.size ? [...set] : ['—'];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderTree() {
|
|
208
|
+
const bag = {};
|
|
209
|
+
for (const c of DATA.contracts) (bag[c.page || '(без страницы)'] ||= []).push(c);
|
|
210
|
+
const opened = new Set();
|
|
211
|
+
for (const c of DATA.contracts) for (const k of c.controls || []) if (k.opens) opened.add(k.opens);
|
|
212
|
+
const { c: sel } = current();
|
|
213
|
+
let html = '';
|
|
214
|
+
for (const [page, list] of Object.entries(bag)) {
|
|
215
|
+
html += '<div class="page" data-group="page">' + page + '</div>';
|
|
216
|
+
for (const c of list.filter((x) => !opened.has(x.id))) html += leaf(c, sel, list, 0);
|
|
217
|
+
}
|
|
218
|
+
$('tree').innerHTML = html;
|
|
219
|
+
function leaf(c, sel, list, depth) {
|
|
220
|
+
let h = '<a href="?form=' + encodeURIComponent(c.id) + '" data-item="contract" data-id="' + c.id +
|
|
221
|
+
'" class="' + (sel && sel.id === c.id ? 'sel' : '') + '">' + c.screen + '<br><span style="font-size:11px;color:' +
|
|
222
|
+
(sel && sel.id === c.id ? '#cfcfc6' : 'var(--dim)') + '">' + c.id + '</span></a>';
|
|
223
|
+
const children = [];
|
|
224
|
+
for (const k of c.controls || []) if (k.opens) { const t = DATA.contracts.find((x) => x.id === k.opens); if (t) children.push(t); }
|
|
225
|
+
if (children.length) h += '<div class="opens">' + children.map((t) => leaf(t, sel, list, depth + 1)).join('') + '</div>';
|
|
226
|
+
return h;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
$('tree').addEventListener('click', (e) => {
|
|
230
|
+
const a = e.target.closest('a[data-id]');
|
|
231
|
+
if (a) { e.preventDefault(); select(a.getAttribute('data-id'), null); }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
function renderDoc() {
|
|
235
|
+
const { c } = current();
|
|
236
|
+
if (!c) { $('doc').innerHTML = '<p>Контрактов не найдено.</p>'; return; }
|
|
237
|
+
let h = '<dl class="kv" data-group="identity">';
|
|
238
|
+
for (const [k, v] of [['id', c.id], ['screen', c.screen], ['page', c.page], ['entity', c.entity]])
|
|
239
|
+
if (v) h += '<dt>' + k + '</dt><dd>' + v + '</dd>';
|
|
240
|
+
h += '</dl>';
|
|
241
|
+
h += '<dl class="kv"><dt>presents — что человек обязан видеть</dt></dl>';
|
|
242
|
+
for (const p of c.presents || [])
|
|
243
|
+
h += '<div class="item" data-item="presents"><div class="name">' + p.field + (p.when ? ' <span class="meta">· ' + p.when.join(', ') + '</span>' : '') +
|
|
244
|
+
'</div><div class="shows">' + p.shows + '</div>' + (p.note ? '<div class="meta">' + p.note + '</div>' : '') + '</div>';
|
|
245
|
+
if ((c.controls || []).length) {
|
|
246
|
+
h += '<dl class="kv"><dt>controls</dt></dl>';
|
|
247
|
+
for (const k of c.controls || [])
|
|
248
|
+
h += '<div class="item" data-item="control"><div class="name">' + k.control +
|
|
249
|
+
(k.shown_when ? ' <span class="meta">· ' + k.shown_when.join(', ') + '</span>' : '') + '</div>' +
|
|
250
|
+
(k.calls ? '<div class="meta">calls: ' + k.calls + '</div>' : '') +
|
|
251
|
+
(k.opens ? '<div class="meta">opens: ' + k.opens + '</div>' : '') +
|
|
252
|
+
(k.shown_when_rule ? '<div class="meta">когда: ' + k.shown_when_rule + '</div>' : '') +
|
|
253
|
+
(k.behaviour ? '<div class="meta">' + k.behaviour + '</div>' : '') + '</div>';
|
|
254
|
+
}
|
|
255
|
+
if ((c.removed || []).length) {
|
|
256
|
+
h += '<dl class="kv"><dt>removed — решено, что этого быть не должно</dt></dl>';
|
|
257
|
+
for (const r of c.removed || []) h += '<div class="item" data-item="removed"><div class="name">' + r.control + '</div><div class="meta">' + (r.was || '') + '</div></div>';
|
|
258
|
+
}
|
|
259
|
+
if (c.__body) h += '<div class="prose" data-group="prose">' + c.__body.replace(/</g, '<') + '</div>';
|
|
260
|
+
$('doc').innerHTML = h;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function renderWire() {
|
|
264
|
+
const { c, state: fromUrl } = current();
|
|
265
|
+
if (!c) { $('wire').innerHTML = ''; $('states').innerHTML = ''; return; }
|
|
266
|
+
const states = statesOf(c);
|
|
267
|
+
const state = states.includes(fromUrl) ? fromUrl : states[0];
|
|
268
|
+
$('states').innerHTML = states.map((s) =>
|
|
269
|
+
'<button data-control="state" data-state="' + s + '" class="' + (s === state ? 'sel' : '') + '">' + s + '</button>').join('');
|
|
270
|
+
const inv = DATA.inventory && DATA.inventory[c.screen];
|
|
271
|
+
const now = inv && inv[state];
|
|
272
|
+
const badge = (kind, label) => '<span class="badge b-' + kind + '">' + label + '</span>';
|
|
273
|
+
let h = '<div class="wire" data-group="wire-canvas">';
|
|
274
|
+
const claimedF = new Set(), claimedC = new Set();
|
|
275
|
+
for (const p of c.presents || []) {
|
|
276
|
+
claimedF.add(p.field);
|
|
277
|
+
if (p.when && !p.when.includes(state)) continue;
|
|
278
|
+
const v = !now ? '' : now.fields.includes(p.field) ? badge('ok', 'есть') : badge('todo', 'нет в коде');
|
|
279
|
+
h += '<div class="el" data-item="wire-field" data-anchor="' + p.field + '"><div class="name">' + p.field + v + '</div><div class="shows">' + p.shows + '</div></div>';
|
|
280
|
+
}
|
|
281
|
+
h += '<div class="btnrow" data-group="wire-controls">';
|
|
282
|
+
for (const k of c.controls || []) {
|
|
283
|
+
claimedC.add(k.control);
|
|
284
|
+
const inState = k.shown_when ? k.shown_when.includes(state) : true;
|
|
285
|
+
if (!inState) continue;
|
|
286
|
+
const v = !now ? '' : now.controls.includes(k.control) ? badge('ok', 'есть') : badge('todo', 'нет в коде');
|
|
287
|
+
h += '<span class="ctl' + (k.shown_when ? '' : ' rule') + '" data-item="wire-control" data-anchor="' + k.control + '">' + k.control + v + '</span>';
|
|
288
|
+
}
|
|
289
|
+
for (const r of c.removed || []) {
|
|
290
|
+
claimedC.add(r.control);
|
|
291
|
+
if (now && now.controls.includes(r.control))
|
|
292
|
+
h += '<span class="ctl removedghost" data-item="wire-removed" data-anchor="' + r.control + '">' + r.control + badge('todo', 'должно исчезнуть') + '</span>';
|
|
293
|
+
}
|
|
294
|
+
h += '</div></div>';
|
|
295
|
+
if (now) {
|
|
296
|
+
const extraF = now.fields.filter((f) => !claimedF.has(f));
|
|
297
|
+
const extraC = now.controls.filter((f) => !claimedC.has(f));
|
|
298
|
+
if (extraF.length || extraC.length) {
|
|
299
|
+
h += '<div class="extras" data-group="wire-extras"><dl class="kv"><dt>отрендерено, но владелец не решал</dt></dl>';
|
|
300
|
+
for (const f of extraF) h += '<div class="el"><div class="name">' + f + badge('extra', 'вне контракта') + '</div></div>';
|
|
301
|
+
if (extraC.length) h += '<div class="btnrow">' + extraC.map((x) => '<span class="ctl rule">' + x + badge('extra', 'вне контракта') + '</span>').join('') + '</div>';
|
|
302
|
+
h += '</div>';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
$('wire').innerHTML = h;
|
|
306
|
+
}
|
|
307
|
+
$('states').addEventListener('click', (e) => {
|
|
308
|
+
const b = e.target.closest('button[data-state]');
|
|
309
|
+
if (b) select(current().c.id, b.getAttribute('data-state'));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
function renderAll() {
|
|
313
|
+
document.getElementById('src').textContent =
|
|
314
|
+
DATA.contracts.length + ' контракт(ов)' + (DATA.producedBy ? ' · перечень: ' + DATA.producedBy : ' · перечня формы нет — каркас без вердиктов');
|
|
315
|
+
renderTree(); renderDoc(); renderWire();
|
|
316
|
+
}
|
|
317
|
+
renderAll();
|
|
318
|
+
</script>
|
|
319
|
+
</body>
|
|
320
|
+
</html>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
export function commandPreview(configPath, outPath, loadConfig) {
|
|
324
|
+
const { config, base } = loadConfig(configPath);
|
|
325
|
+
const data = collectPreviewData(config, base);
|
|
326
|
+
if (data.contracts.length === 0) {
|
|
327
|
+
console.error("usedesign preview: the config's `forms` patterns matched no contract");
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
writeFileSync(outPath, renderPreviewHtml(data), "utf8");
|
|
331
|
+
console.log(`preview: ${data.contracts.length} contract(s) → ${outPath}${data.inventory ? " (с вердиктами по перечню формы)" : " (перечня формы нет — без вердиктов)"}`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Config, Finding } from "./core.js";
|
|
2
|
+
export interface ScaffoldPlan {
|
|
3
|
+
drafts: {
|
|
4
|
+
id: string;
|
|
5
|
+
file: string;
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}[];
|
|
10
|
+
skippedDeclared: {
|
|
11
|
+
route: string;
|
|
12
|
+
by: string[];
|
|
13
|
+
}[];
|
|
14
|
+
skippedExcluded: {
|
|
15
|
+
route: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}[];
|
|
18
|
+
existing: string[];
|
|
19
|
+
total: number;
|
|
20
|
+
findings: Finding[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Work out what would be written, without writing it. The counts are the product: generated plus
|
|
24
|
+
* skipped-because-declared plus skipped-because-excluded must equal the number of routes served,
|
|
25
|
+
* and a command that quietly drops the difference is indistinguishable from one that covered
|
|
26
|
+
* everything.
|
|
27
|
+
*/
|
|
28
|
+
export declare function planScaffold(openapiPath: string, outDir: string, config: Config | null, base: string | null): ScaffoldPlan;
|
|
29
|
+
/** Write the planned drafts. Existing files are left alone unless `force` — a half-filled draft
|
|
30
|
+
* is somebody's work, and silently replacing it is the one failure this command could cause that
|
|
31
|
+
* nobody would notice until the writing was gone. */
|
|
32
|
+
export declare function writeScaffold(plan: ScaffoldPlan, outDir: string, force: boolean): {
|
|
33
|
+
written: number;
|
|
34
|
+
kept: number;
|
|
35
|
+
};
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draft generation — turning a served route into the *shell* of a card.
|
|
3
|
+
*
|
|
4
|
+
* The point is arithmetic, not authorship. Describing 113 routes from a blank page is work
|
|
5
|
+
* nobody starts; filling in the meaning of 113 shells is work somebody finishes. So this module
|
|
6
|
+
* writes down what the machine can read and refuses to write down anything else.
|
|
7
|
+
*
|
|
8
|
+
* WHY OPENAPI HERE, WHEN THE CHECKS DELIBERATELY DO NOT READ IT
|
|
9
|
+
* The three checks compare cards against a route *inventory* — a flat statement the repository
|
|
10
|
+
* produces about itself — precisely so that no framework is ever parsed. Scaffolding is not
|
|
11
|
+
* checking: nothing here decides whether anything is true, and a draft that reads a route wrong
|
|
12
|
+
* is caught the moment a human looks at it or a checker runs. Reading the richer document is
|
|
13
|
+
* therefore allowed for drafts and still forbidden for verdicts.
|
|
14
|
+
*
|
|
15
|
+
* THE DRAFT MUST NOT PASS VALIDATION
|
|
16
|
+
* Every field a machine cannot know is written as `FILL_ME`, which fails the schema on purpose.
|
|
17
|
+
* A draft that validated would be the worst possible output of this command: 113 documents that
|
|
18
|
+
* look complete, claim maturity nobody measured, and are wrong in a way that reads as machine
|
|
19
|
+
* precision. Screaming is the feature.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { Finding, loadCardFiles } from "./core.js";
|
|
24
|
+
import { normalise } from "./checks.js";
|
|
25
|
+
const VERBS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
26
|
+
/**
|
|
27
|
+
* A provisional id built from the route shape. It is deliberately not a guess at the operation's
|
|
28
|
+
* name: `TODO` is upper-case, so the id fails its own pattern and the card says out loud that a
|
|
29
|
+
* human has not been here yet. Uniqueness and stability matter — the same OpenAPI must always
|
|
30
|
+
* produce the same filenames, or every regeneration is a diff nobody can read.
|
|
31
|
+
*/
|
|
32
|
+
function provisionalId(method, path, taken) {
|
|
33
|
+
// `{id}` carries no meaning for a name; `:finish` does — under AIP-136 the colon suffix is the
|
|
34
|
+
// action, and the checker's own normaliser is careful never to eat it (see checks.ts PARAM).
|
|
35
|
+
const slug = path
|
|
36
|
+
.replace(/\{[^}]*\}/g, "")
|
|
37
|
+
.split(/[/:]+/)
|
|
38
|
+
.filter((s) => s.length > 0)
|
|
39
|
+
.join("-")
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
42
|
+
.replace(/-+/g, "-")
|
|
43
|
+
.replace(/^-|-$/g, "") || "root";
|
|
44
|
+
const base = `TODO.${slug}.${method.toLowerCase()}`;
|
|
45
|
+
if (!taken.has(base))
|
|
46
|
+
return base;
|
|
47
|
+
for (let n = 2;; n += 1) {
|
|
48
|
+
const candidate = `${base}-${n}`;
|
|
49
|
+
if (!taken.has(candidate))
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Status codes the document itself lists. `default` and anything non-numeric are not statuses. */
|
|
54
|
+
function responsesOf(operation) {
|
|
55
|
+
return Object.keys(operation?.responses ?? {})
|
|
56
|
+
.map((code) => Number.parseInt(code, 10))
|
|
57
|
+
.filter((code) => Number.isInteger(code) && code >= 100 && code <= 599)
|
|
58
|
+
.sort((a, b) => a - b);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Does the document actually *say* anything about statuses, or does it merely default?
|
|
62
|
+
*
|
|
63
|
+
* Measured on the first repository this ran against: all 113 routes declared exactly `200` and
|
|
64
|
+
* nothing else, while the code demonstrably answers 401 and 422 — the cards written by reading
|
|
65
|
+
* that code say so, and the checks pass. A status list identical across every route in the
|
|
66
|
+
* document is the signature of a framework filling in a blank, not of anybody describing
|
|
67
|
+
* behaviour. Copying it into a draft unmarked would hand a human a falsehood wearing the clothes
|
|
68
|
+
* of machine precision, which is the one output this command must never produce.
|
|
69
|
+
*/
|
|
70
|
+
function statusesAreDegenerate(routes) {
|
|
71
|
+
if (routes.length < 2)
|
|
72
|
+
return false;
|
|
73
|
+
const sets = new Set(routes.map((r) => responsesOf(r.operation).join(",")));
|
|
74
|
+
return sets.size === 1;
|
|
75
|
+
}
|
|
76
|
+
function draftBody(id, method, path, operation, source, degenerate) {
|
|
77
|
+
const responses = responsesOf(operation);
|
|
78
|
+
const parameters = (operation?.parameters ?? [])
|
|
79
|
+
.filter((p) => p?.in === "path" && p?.name)
|
|
80
|
+
.map((p) => String(p.name));
|
|
81
|
+
// What the document says about itself goes in the prose, attributed, never into a field. A
|
|
82
|
+
// summary copied into `title` would fill a human's field with a machine's guess, and the next
|
|
83
|
+
// reader could not tell which of the two wrote it.
|
|
84
|
+
const says = [];
|
|
85
|
+
if (operation?.summary)
|
|
86
|
+
says.push(`summary: ${String(operation.summary).trim()}`);
|
|
87
|
+
if (operation?.operationId)
|
|
88
|
+
says.push(`operationId: ${String(operation.operationId).trim()}`);
|
|
89
|
+
if (parameters.length)
|
|
90
|
+
says.push(`параметры пути: ${parameters.join(", ")}`);
|
|
91
|
+
const lines = [
|
|
92
|
+
"---",
|
|
93
|
+
"# ЗАГОТОВКА, а не карточка. Машина записала только то, что прочитала в OpenAPI;",
|
|
94
|
+
"# всё остальное — FILL_ME, и пока они здесь, файл НЕ проходит `usedesign validate`.",
|
|
95
|
+
"# Это сделано намеренно: заготовка, прошедшая проверку, выглядела бы описанием, не будучи им.",
|
|
96
|
+
"#",
|
|
97
|
+
"# Дозаполнив, ПЕРЕНЕСИТЕ файл в cards/ — переезд и есть слово «я за это отвечаю».",
|
|
98
|
+
`id: ${id}`,
|
|
99
|
+
"title: FILL_ME",
|
|
100
|
+
"scenario: FILL_ME # либо serves_step, если операция служит шагу другой",
|
|
101
|
+
"actors: [FILL_ME]",
|
|
102
|
+
"",
|
|
103
|
+
"maturity: FILL_ME # conceived | designed | implemented | tested | in_production",
|
|
104
|
+
"",
|
|
105
|
+
"steps:",
|
|
106
|
+
" - id: s1-FILL-ME",
|
|
107
|
+
" text: FILL_ME",
|
|
108
|
+
"",
|
|
109
|
+
"concurrency:",
|
|
110
|
+
" mode: FILL_ME # etag_required | idempotency_by_header | idempotency_by_formula | none_by_design",
|
|
111
|
+
" rationale: FILL_ME",
|
|
112
|
+
" source: FILL_ME",
|
|
113
|
+
"",
|
|
114
|
+
"interfaces:",
|
|
115
|
+
" rest:",
|
|
116
|
+
" transport: http_rest",
|
|
117
|
+
` method: ${method.toUpperCase()}`,
|
|
118
|
+
` path: ${path}`,
|
|
119
|
+
responses.length
|
|
120
|
+
? ` responses: [${responses.join(", ")}]${degenerate ? " # ⚠ см. ниже: документ назвал ОДИН И ТОТ ЖЕ набор для всех маршрутов" : ""}`
|
|
121
|
+
: " responses: [] # документ не назвал ни одного статуса — проверить руками",
|
|
122
|
+
"",
|
|
123
|
+
"data:",
|
|
124
|
+
" entities: [FILL_ME]",
|
|
125
|
+
"provenance: FILL_ME # none для чтения, либо объект с activity_kind",
|
|
126
|
+
"reversibility: FILL_ME # reversible | irreversible | not_applicable | {reversible_via: …}",
|
|
127
|
+
"---",
|
|
128
|
+
"",
|
|
129
|
+
`Заготовка для маршрута \`${method.toUpperCase()} ${path}\`, снятого из \`${source}\`.`,
|
|
130
|
+
"",
|
|
131
|
+
says.length ? `Документ говорит о себе: ${says.join(" · ")}.` : "Документ не сообщает о маршруте ничего сверх адреса и статусов.",
|
|
132
|
+
"",
|
|
133
|
+
"Машина не знает, зачем эта операция существует, какие проверки она делает и чем доказана её",
|
|
134
|
+
"зрелость. Эти поля ждут человека, и до него описание не является описанием.",
|
|
135
|
+
"",
|
|
136
|
+
...(degenerate
|
|
137
|
+
? [
|
|
138
|
+
"⚠️ `responses` СКОПИРОВАН, НО ЕМУ НЕЛЬЗЯ ВЕРИТЬ. Документ назвал один и тот же набор",
|
|
139
|
+
"статусов для КАЖДОГО маршрута приложения — так выглядит не описание поведения, а",
|
|
140
|
+
"умолчание фреймворка. Реальные коды ошибок ищите в коде: заполнив `steps[]` с",
|
|
141
|
+
"`on_violation.http`, вы почти наверняка обнаружите статусы, которых здесь нет, и",
|
|
142
|
+
"проверка `undeclared_response` это покажет.",
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
: []),
|
|
146
|
+
];
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Work out what would be written, without writing it. The counts are the product: generated plus
|
|
151
|
+
* skipped-because-declared plus skipped-because-excluded must equal the number of routes served,
|
|
152
|
+
* and a command that quietly drops the difference is indistinguishable from one that covered
|
|
153
|
+
* everything.
|
|
154
|
+
*/
|
|
155
|
+
export function planScaffold(openapiPath, outDir, config, base) {
|
|
156
|
+
const findings = [];
|
|
157
|
+
const doc = JSON.parse(readFileSync(openapiPath, "utf8"));
|
|
158
|
+
const declared = new Map();
|
|
159
|
+
if (config && base) {
|
|
160
|
+
const { cards, findings: cardFindings } = loadCardFiles(config, base);
|
|
161
|
+
findings.push(...cardFindings.filter((f) => f.code !== "no_cards_found"));
|
|
162
|
+
for (const [cardId, fm] of cards) {
|
|
163
|
+
for (const iface of Object.values(fm["interfaces"] ?? {})) {
|
|
164
|
+
if (iface?.transport !== "http_rest" || !iface.method || !iface.path)
|
|
165
|
+
continue;
|
|
166
|
+
// One route may be declared by several cards (dispatch). It is still ONE covered route:
|
|
167
|
+
// counting cards here would make the arithmetic add up for the wrong reason.
|
|
168
|
+
const key = normalise(iface.method, iface.path);
|
|
169
|
+
declared.set(key, [...(declared.get(key) ?? []), cardId]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const routes = [];
|
|
174
|
+
for (const [path, operations] of Object.entries(doc.paths ?? {})) {
|
|
175
|
+
for (const [verb, operation] of Object.entries(operations ?? {})) {
|
|
176
|
+
if (VERBS.includes(verb.toLowerCase()))
|
|
177
|
+
routes.push({ method: verb.toUpperCase(), path, operation });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
routes.sort((a, b) => (a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)));
|
|
181
|
+
if (routes.length === 0) {
|
|
182
|
+
findings.push(new Finding("openapi_empty", `${openapiPath}: no routes — almost always a failed dump rather than an application with no routes`));
|
|
183
|
+
}
|
|
184
|
+
const plan = {
|
|
185
|
+
drafts: [],
|
|
186
|
+
skippedDeclared: [],
|
|
187
|
+
skippedExcluded: [],
|
|
188
|
+
existing: [],
|
|
189
|
+
total: routes.length,
|
|
190
|
+
findings,
|
|
191
|
+
};
|
|
192
|
+
const taken = new Set();
|
|
193
|
+
const source = openapiPath.split(/[\\/]/).pop() ?? openapiPath;
|
|
194
|
+
const degenerate = statusesAreDegenerate(routes);
|
|
195
|
+
if (degenerate) {
|
|
196
|
+
findings.push(new Finding("statuses_uniform", `every one of the ${routes.length} routes declares the same statuses (${responsesOf(routes[0].operation).join(", ") || "none"}) — a framework default rather than a description; each draft says so`, "warning"));
|
|
197
|
+
}
|
|
198
|
+
for (const { method, path, operation } of routes) {
|
|
199
|
+
const key = normalise(method, path);
|
|
200
|
+
const shape = key.slice(key.indexOf(" ") + 1);
|
|
201
|
+
const rule = (config?.exclude ?? []).find((r) => {
|
|
202
|
+
if (r.method && r.method.toUpperCase() !== method)
|
|
203
|
+
return false;
|
|
204
|
+
const escaped = (r.path ?? "").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
205
|
+
return new RegExp(`^${escaped}$`).test(shape);
|
|
206
|
+
});
|
|
207
|
+
if (rule) {
|
|
208
|
+
plan.skippedExcluded.push({ route: key, reason: rule.reason ?? "(без причины в конфиге)" });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const owners = declared.get(key);
|
|
212
|
+
if (owners) {
|
|
213
|
+
plan.skippedDeclared.push({ route: key, by: owners });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const id = provisionalId(method, path, taken);
|
|
217
|
+
taken.add(id);
|
|
218
|
+
const file = join(outDir, `${id}.op.md`);
|
|
219
|
+
if (existsSync(file))
|
|
220
|
+
plan.existing.push(file);
|
|
221
|
+
plan.drafts.push({ id, file, method, path, body: draftBody(id, method, path, operation, source, degenerate) });
|
|
222
|
+
}
|
|
223
|
+
return plan;
|
|
224
|
+
}
|
|
225
|
+
/** Write the planned drafts. Existing files are left alone unless `force` — a half-filled draft
|
|
226
|
+
* is somebody's work, and silently replacing it is the one failure this command could cause that
|
|
227
|
+
* nobody would notice until the writing was gone. */
|
|
228
|
+
export function writeScaffold(plan, outDir, force) {
|
|
229
|
+
mkdirSync(outDir, { recursive: true });
|
|
230
|
+
let written = 0;
|
|
231
|
+
let kept = 0;
|
|
232
|
+
for (const draft of plan.drafts) {
|
|
233
|
+
if (existsSync(draft.file) && !force) {
|
|
234
|
+
kept += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
writeFileSync(draft.file, draft.body, { encoding: "utf8" });
|
|
238
|
+
written += 1;
|
|
239
|
+
}
|
|
240
|
+
return { written, kept };
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAU,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAW3E;;;;;GAKG;AACH,SAAS,aAAa,CAAC,MAAc,EAAE,IAAY,EAAE,KAAkB;IACrE,+FAA+F;IAC/F,6FAA6F;IAC7F,MAAM,IAAI,GACR,IAAI;SACD,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SACzB,KAAK,CAAC,OAAO,CAAC;SACd,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,IAAI,CAAC,GAAG,CAAC;SACT,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;IAErC,MAAM,IAAI,GAAG,QAAQ,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;IACpD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,SAAS,WAAW,CAAC,SAAc;IACjC,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,IAAI,EAAE,CAAC;SAC3C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;SACxC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC;SACtE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,qBAAqB,CAAC,MAA4B;IACzD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5E,OAAO,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,SAAS,CAAC,EAAU,EAAE,MAAc,EAAE,IAAY,EAAE,SAAc,EAAE,MAAc,EAAE,UAAmB;IAC9G,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,UAAU,GAAa,CAAC,SAAS,EAAE,UAAU,IAAI,EAAE,CAAC;SACvD,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,MAAM,IAAI,CAAC,EAAE,IAAI,CAAC;SAC/C,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnC,2FAA2F;IAC3F,8FAA8F;IAC9F,mDAAmD;IACnD,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,SAAS,EAAE,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClF,IAAI,SAAS,EAAE,WAAW;QAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC9F,IAAI,UAAU,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,mBAAmB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE7E,MAAM,KAAK,GAAG;QACZ,KAAK;QACL,iFAAiF;QACjF,qFAAqF;QACrF,+FAA+F;QAC/F,GAAG;QACH,mFAAmF;QACnF,OAAO,EAAE,EAAE;QACX,gBAAgB;QAChB,iFAAiF;QACjF,mBAAmB;QACnB,EAAE;QACF,0FAA0F;QAC1F,EAAE;QACF,QAAQ;QACR,oBAAoB;QACpB,mBAAmB;QACnB,EAAE;QACF,cAAc;QACd,8GAA8G;QAC9G,sBAAsB;QACtB,mBAAmB;QACnB,EAAE;QACF,aAAa;QACb,SAAS;QACT,0BAA0B;QAC1B,eAAe,MAAM,CAAC,WAAW,EAAE,EAAE;QACrC,aAAa,IAAI,EAAE;QACnB,SAAS,CAAC,MAAM;YACd,CAAC,CAAC,mBAAmB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,yEAAyE,CAAC,CAAC,CAAC,EAAE,EAAE;YAC1I,CAAC,CAAC,wFAAwF;QAC5F,EAAE;QACF,OAAO;QACP,uBAAuB;QACvB,2EAA2E;QAC3E,+FAA+F;QAC/F,KAAK;QACL,EAAE;QACF,4BAA4B,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,oBAAoB,MAAM,KAAK;QACvF,EAAE;QACF,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,4BAA4B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,iEAAiE;QACjI,EAAE;QACF,6FAA6F;QAC7F,6EAA6E;QAC7E,EAAE;QACF,GAAG,CAAC,UAAU;YACZ,CAAC,CAAC;gBACE,sFAAsF;gBACtF,kFAAkF;gBAClF,+EAA+E;gBAC/E,kFAAkF;gBAClF,6CAA6C;gBAC7C,EAAE;aACH;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,WAAmB,EACnB,MAAc,EACd,MAAqB,EACrB,IAAmB;IAEnB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAE1D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC7C,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACtE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,gBAAgB,CAAC,CAAC,CAAC;QAC1E,KAAK,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAM,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC/D,IAAI,KAAK,EAAE,SAAS,KAAK,WAAW,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI;oBAAE,SAAS;gBAC/E,wFAAwF;gBACxF,6EAA6E;gBAC7E,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBAChD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAuD,EAAE,CAAC;IACtE,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAM,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACtE,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAM,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;YACtE,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE7G,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CACX,IAAI,OAAO,CAAC,eAAe,EAAE,GAAG,WAAW,qFAAqF,CAAC,CAClI,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAiB;QACzB,MAAM,EAAE,EAAE;QACV,eAAe,EAAE,EAAE;QACnB,eAAe,EAAE,EAAE;QACnB,QAAQ,EAAE,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,QAAQ;KACT,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC;IAC/D,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACjD,IAAI,UAAU,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,CACX,IAAI,OAAO,CACT,kBAAkB,EAClB,oBAAoB,MAAM,CAAC,MAAM,uCAAuC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,uEAAuE,EACrM,SAAS,CACV,CACF,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,MAAM,EAAE,CAAC;QACjD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAE9C,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YAC9C,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM;gBAAE,OAAO,KAAK,CAAC;YAChE,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACnH,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QACH,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,yBAAyB,EAAE,CAAC,CAAC;YAC5F,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YACtD,SAAS;QACX,CAAC;QAED,MAAM,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9C,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACd,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QACzC,IAAI,UAAU,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IACjH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;qDAEqD;AACrD,MAAM,UAAU,aAAa,CAAC,IAAkB,EAAE,MAAc,EAAE,KAAc;IAC9E,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,CAAC;YACV,SAAS;QACX,CAAC;QACD,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5D,OAAO,IAAI,CAAC,CAAC;IACf,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
|