@codexa/cli 9.0.3 → 9.0.4
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/commands/architect.ts +7 -6
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -0
- package/commands/decide.ts +9 -21
- package/commands/discover.ts +11 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +190 -75
- package/commands/patterns.ts +6 -13
- package/commands/plan.ts +14 -64
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +2 -3
- package/commands/task.ts +30 -9
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -708
- package/db/schema.test.ts +475 -48
- package/db/schema.ts +136 -12
- package/gates/validator.test.ts +58 -0
- package/gates/validator.ts +83 -30
- package/package.json +1 -1
- package/workflow.ts +102 -51
package/workflow.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { checkRequest, checkApprove, checkReject } from "./commands/check";
|
|
|
6
6
|
import { taskNext, taskStart, taskDone } from "./commands/task";
|
|
7
7
|
import { decide, listDecisions } from "./commands/decide";
|
|
8
8
|
import { reviewStart, reviewApprove, reviewSkip } from "./commands/review";
|
|
9
|
-
import { addKnowledge, listKnowledge, acknowledgeKnowledge, queryGraph, resolveKnowledge } from "./commands/knowledge";
|
|
9
|
+
import { addKnowledge, listKnowledge, acknowledgeKnowledge, queryGraph, resolveKnowledge, compactKnowledge } from "./commands/knowledge";
|
|
10
10
|
import { status, contextExport, recover, contextUpdate, contextDetail } from "./commands/utils";
|
|
11
11
|
import {
|
|
12
12
|
discoverStart,
|
|
@@ -40,12 +40,12 @@ import {
|
|
|
40
40
|
architectCancel,
|
|
41
41
|
} from "./commands/architect";
|
|
42
42
|
import { initSchema } from "./db/schema";
|
|
43
|
-
import { getDb } from "./db/connection";
|
|
43
|
+
import { getDb, closeDb } from "./db/connection";
|
|
44
44
|
import { execSync } from "child_process";
|
|
45
45
|
import { existsSync, readFileSync } from "fs";
|
|
46
46
|
import { join } from "path";
|
|
47
47
|
import pkg from "./package.json";
|
|
48
|
-
import { CodexaError, ValidationError } from "./errors";
|
|
48
|
+
import { CodexaError, ValidationError, GateError } from "./errors";
|
|
49
49
|
|
|
50
50
|
function checkVersionSync(): void {
|
|
51
51
|
// 1. Check CLI vs Plugin (dev repo only)
|
|
@@ -103,9 +103,27 @@ function checkVersionSync(): void {
|
|
|
103
103
|
function handleError(e: unknown): never {
|
|
104
104
|
if (e instanceof CodexaError) {
|
|
105
105
|
console.error(`\n${e.message}\n`);
|
|
106
|
+
|
|
107
|
+
// v9.3: Exibir diagnostico e passos de recuperacao para falhas de gate
|
|
108
|
+
if (e instanceof GateError && e.recovery) {
|
|
109
|
+
console.error("\u2500".repeat(50));
|
|
110
|
+
console.error("DIAGNOSTICO:");
|
|
111
|
+
console.error(` ${e.recovery.diagnostic}\n`);
|
|
112
|
+
console.error("PASSOS PARA CORRIGIR:");
|
|
113
|
+
for (const step of e.recovery.steps) {
|
|
114
|
+
console.error(` \u2192 ${step}`);
|
|
115
|
+
}
|
|
116
|
+
if (e.recovery.command) {
|
|
117
|
+
console.error(`\nPara mais detalhes: ${e.recovery.command}`);
|
|
118
|
+
}
|
|
119
|
+
console.error();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
closeDb();
|
|
106
123
|
process.exit(e.exitCode);
|
|
107
124
|
}
|
|
108
125
|
console.error(`\n[ERRO INESPERADO] ${(e as Error).message}\n`);
|
|
126
|
+
closeDb();
|
|
109
127
|
process.exit(1);
|
|
110
128
|
}
|
|
111
129
|
|
|
@@ -114,10 +132,14 @@ function wrapAction<T extends (...args: any[]) => any>(fn: T): T {
|
|
|
114
132
|
try {
|
|
115
133
|
const result = fn(...args);
|
|
116
134
|
if (result instanceof Promise) {
|
|
117
|
-
return result.catch(
|
|
135
|
+
return result.catch((e) => {
|
|
136
|
+
closeDb();
|
|
137
|
+
handleError(e);
|
|
138
|
+
});
|
|
118
139
|
}
|
|
119
140
|
return result;
|
|
120
141
|
} catch (e) {
|
|
142
|
+
closeDb();
|
|
121
143
|
handleError(e);
|
|
122
144
|
}
|
|
123
145
|
}) as T;
|
|
@@ -130,15 +152,7 @@ program
|
|
|
130
152
|
.description(`Codexa Workflow v${pkg.version} - Sistema de workflow para Claude Code`)
|
|
131
153
|
.version(pkg.version)
|
|
132
154
|
.hook("preAction", () => {
|
|
133
|
-
|
|
134
|
-
checkVersionSync();
|
|
135
|
-
} catch (e) {
|
|
136
|
-
if (e instanceof CodexaError) {
|
|
137
|
-
console.error(`\n${e.message}\n`);
|
|
138
|
-
process.exit(e.exitCode);
|
|
139
|
-
}
|
|
140
|
-
throw e;
|
|
141
|
-
}
|
|
155
|
+
checkVersionSync();
|
|
142
156
|
});
|
|
143
157
|
|
|
144
158
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -160,8 +174,9 @@ planCmd
|
|
|
160
174
|
.command("show")
|
|
161
175
|
.description("Mostra o plano atual")
|
|
162
176
|
.option("--json", "Saida em JSON")
|
|
177
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
163
178
|
.action((options) => {
|
|
164
|
-
planShow(options.json);
|
|
179
|
+
planShow(options.json, options.spec);
|
|
165
180
|
});
|
|
166
181
|
|
|
167
182
|
planCmd
|
|
@@ -172,15 +187,17 @@ planCmd
|
|
|
172
187
|
.option("--depends <ids>", "IDs das tasks que esta depende (ex: 1,2)")
|
|
173
188
|
.option("--files <files>", "Arquivos esperados (ex: src/a.ts,src/b.ts)")
|
|
174
189
|
.option("--sequential", "Marca como sequencial (nao paralelizavel)")
|
|
190
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
175
191
|
.action((options) => {
|
|
176
|
-
planTaskAdd(options);
|
|
192
|
+
planTaskAdd({ ...options, specId: options.spec });
|
|
177
193
|
});
|
|
178
194
|
|
|
179
195
|
planCmd
|
|
180
196
|
.command("cancel")
|
|
181
197
|
.description("Cancela a feature atual (soft-cancel, preserva historico)")
|
|
182
|
-
.
|
|
183
|
-
|
|
198
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
199
|
+
.action((options) => {
|
|
200
|
+
planCancel(options.spec);
|
|
184
201
|
});
|
|
185
202
|
|
|
186
203
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -192,22 +209,25 @@ const checkCmd = program.command("check").description("Comandos da fase CHECK");
|
|
|
192
209
|
checkCmd
|
|
193
210
|
.command("request")
|
|
194
211
|
.description("Solicita aprovacao do plano")
|
|
195
|
-
.
|
|
196
|
-
|
|
212
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
213
|
+
.action((options) => {
|
|
214
|
+
checkRequest(options.spec);
|
|
197
215
|
});
|
|
198
216
|
|
|
199
217
|
checkCmd
|
|
200
218
|
.command("approve")
|
|
201
219
|
.description("Aprova o plano")
|
|
202
|
-
.
|
|
203
|
-
|
|
220
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
221
|
+
.action((options) => {
|
|
222
|
+
checkApprove(options.spec);
|
|
204
223
|
});
|
|
205
224
|
|
|
206
225
|
checkCmd
|
|
207
226
|
.command("reject <reason>")
|
|
208
227
|
.description("Rejeita o plano com motivo")
|
|
209
|
-
.
|
|
210
|
-
|
|
228
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
229
|
+
.action((reason: string, options) => {
|
|
230
|
+
checkReject(reason, options.spec);
|
|
211
231
|
});
|
|
212
232
|
|
|
213
233
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -220,8 +240,9 @@ taskCmd
|
|
|
220
240
|
.command("next")
|
|
221
241
|
.description("Mostra proximas tasks disponiveis")
|
|
222
242
|
.option("--json", "Saida em JSON")
|
|
243
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
223
244
|
.action(wrapAction((options) => {
|
|
224
|
-
taskNext(options.json);
|
|
245
|
+
taskNext(options.json, options.spec);
|
|
225
246
|
}));
|
|
226
247
|
|
|
227
248
|
taskCmd
|
|
@@ -229,8 +250,9 @@ taskCmd
|
|
|
229
250
|
.description("Inicia task(s) - pode ser multiplas separadas por virgula")
|
|
230
251
|
.option("--json", "Saida em JSON")
|
|
231
252
|
.option("--full-context", "Incluir contexto completo (modo legado)")
|
|
253
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
232
254
|
.action(wrapAction((ids: string, options) => {
|
|
233
|
-
taskStart(ids, options.json, options.fullContext);
|
|
255
|
+
taskStart(ids, options.json, options.fullContext, options.spec);
|
|
234
256
|
}));
|
|
235
257
|
|
|
236
258
|
taskCmd
|
|
@@ -290,16 +312,18 @@ program
|
|
|
290
312
|
.description("Registra uma decisao")
|
|
291
313
|
.option("--rationale <text>", "Justificativa da decisao")
|
|
292
314
|
.option("--force", "Ignorar deteccao de conflitos")
|
|
315
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
293
316
|
.action((title: string, decision: string, options) => {
|
|
294
|
-
decide(title, decision, options);
|
|
317
|
+
decide(title, decision, { ...options, specId: options.spec });
|
|
295
318
|
});
|
|
296
319
|
|
|
297
320
|
program
|
|
298
321
|
.command("decisions")
|
|
299
322
|
.description("Lista decisoes")
|
|
300
323
|
.option("--json", "Saida em JSON")
|
|
324
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
301
325
|
.action((options) => {
|
|
302
|
-
listDecisions(options.json);
|
|
326
|
+
listDecisions(options.json, options.spec);
|
|
303
327
|
});
|
|
304
328
|
|
|
305
329
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -315,12 +339,14 @@ knowledgeCmd
|
|
|
315
339
|
.requiredOption("--category <cat>", "Categoria (discovery, decision, blocker, pattern, constraint)")
|
|
316
340
|
.option("--severity <level>", "Severidade (info, warning, critical)", "info")
|
|
317
341
|
.option("--broadcast <target>", "Destino (all ou IDs separados por virgula)", "all")
|
|
342
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
318
343
|
.action((options) => {
|
|
319
344
|
addKnowledge({
|
|
320
345
|
content: options.content,
|
|
321
346
|
category: options.category,
|
|
322
347
|
severity: options.severity,
|
|
323
348
|
broadcastTo: options.broadcast,
|
|
349
|
+
specId: options.spec,
|
|
324
350
|
});
|
|
325
351
|
});
|
|
326
352
|
|
|
@@ -331,23 +357,26 @@ knowledgeCmd
|
|
|
331
357
|
.option("--category <cat>", "Filtrar por categoria")
|
|
332
358
|
.option("--severity <level>", "Filtrar por severidade (critical, warning, info)")
|
|
333
359
|
.option("--json", "Saida em JSON")
|
|
360
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
334
361
|
.action((options) => {
|
|
335
|
-
listKnowledge(options);
|
|
362
|
+
listKnowledge({ ...options, specId: options.spec });
|
|
336
363
|
});
|
|
337
364
|
|
|
338
365
|
knowledgeCmd
|
|
339
366
|
.command("ack <id>")
|
|
340
367
|
.description("Marca knowledge como lido pela task atual")
|
|
341
|
-
.
|
|
342
|
-
|
|
368
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
369
|
+
.action((id: string, options) => {
|
|
370
|
+
acknowledgeKnowledge(id, options.spec);
|
|
343
371
|
});
|
|
344
372
|
|
|
345
373
|
knowledgeCmd
|
|
346
374
|
.command("resolve <ids>")
|
|
347
375
|
.description("Resolve/reconhece knowledge item(s) critico(s)")
|
|
348
376
|
.option("--resolution <text>", "Descricao de como o blocker foi resolvido")
|
|
349
|
-
.
|
|
350
|
-
|
|
377
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
378
|
+
.action((ids: string, opts: { resolution?: string; spec?: string }) => {
|
|
379
|
+
resolveKnowledge(ids, opts.resolution, opts.spec);
|
|
351
380
|
});
|
|
352
381
|
|
|
353
382
|
knowledgeCmd
|
|
@@ -356,8 +385,19 @@ knowledgeCmd
|
|
|
356
385
|
.option("--file <path>", "Buscar relacoes de um arquivo")
|
|
357
386
|
.option("--decision <id>", "Buscar arquivos afetados por uma decisao")
|
|
358
387
|
.option("--json", "Saida em JSON")
|
|
388
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
389
|
+
.action((options) => {
|
|
390
|
+
queryGraph({ ...options, specId: options.spec });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
knowledgeCmd
|
|
394
|
+
.command("compact")
|
|
395
|
+
.description("Compacta knowledge (merge similares, arquivar antigos)")
|
|
396
|
+
.option("--spec <id>", "ID do spec (padrao: todos)")
|
|
397
|
+
.option("--dry-run", "Apenas mostrar o que seria compactado")
|
|
398
|
+
.option("--json", "Saida em JSON")
|
|
359
399
|
.action((options) => {
|
|
360
|
-
|
|
400
|
+
compactKnowledge({ specId: options.spec, dryRun: options.dryRun, json: options.json });
|
|
361
401
|
});
|
|
362
402
|
|
|
363
403
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -370,22 +410,27 @@ reviewCmd
|
|
|
370
410
|
.command("start")
|
|
371
411
|
.description("Inicia o review")
|
|
372
412
|
.option("--json", "Saida em JSON")
|
|
413
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
373
414
|
.action((options) => {
|
|
374
|
-
reviewStart(options.json);
|
|
415
|
+
reviewStart(options.json, options.spec);
|
|
375
416
|
});
|
|
376
417
|
|
|
377
418
|
reviewCmd
|
|
378
419
|
.command("approve")
|
|
379
420
|
.description("Aprova o review e finaliza a feature")
|
|
380
|
-
.
|
|
381
|
-
|
|
421
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
422
|
+
.option("--force", "Aprovar mesmo com score baixo (<50)")
|
|
423
|
+
.option("--force-reason <reason>", "Motivo para aprovacao forcada")
|
|
424
|
+
.action((options) => {
|
|
425
|
+
reviewApprove({ specId: options.spec, force: options.force, forceReason: options.forceReason });
|
|
382
426
|
});
|
|
383
427
|
|
|
384
428
|
reviewCmd
|
|
385
429
|
.command("skip")
|
|
386
430
|
.description("Pula o review e finaliza a feature diretamente")
|
|
387
|
-
.
|
|
388
|
-
|
|
431
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
432
|
+
.action((options) => {
|
|
433
|
+
reviewSkip(options.spec);
|
|
389
434
|
});
|
|
390
435
|
|
|
391
436
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -396,8 +441,9 @@ program
|
|
|
396
441
|
.command("status")
|
|
397
442
|
.description("Mostra status atual")
|
|
398
443
|
.option("--json", "Saida em JSON")
|
|
444
|
+
.option("--spec <id>", "ID do spec (padrao: todos ativos)")
|
|
399
445
|
.action((options) => {
|
|
400
|
-
status(options.json);
|
|
446
|
+
status(options.json, options.spec);
|
|
401
447
|
});
|
|
402
448
|
|
|
403
449
|
const contextCmd = program.command("context").description("Comandos de contexto");
|
|
@@ -406,8 +452,9 @@ contextCmd
|
|
|
406
452
|
.command("export")
|
|
407
453
|
.description("Exporta contexto para subagent")
|
|
408
454
|
.option("--task <id>", "ID da task especifica")
|
|
455
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
409
456
|
.action((options) => {
|
|
410
|
-
contextExport({ ...options, json: true }); // Sempre JSON
|
|
457
|
+
contextExport({ ...options, json: true, specId: options.spec }); // Sempre JSON
|
|
411
458
|
});
|
|
412
459
|
|
|
413
460
|
contextCmd
|
|
@@ -416,16 +463,18 @@ contextCmd
|
|
|
416
463
|
.option("--approach <text>", "Adiciona nova abordagem descoberta")
|
|
417
464
|
.option("--pattern <text>", "Registra padrao identificado no codigo")
|
|
418
465
|
.option("--constraint <text>", "Adiciona nova limitacao encontrada")
|
|
466
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
419
467
|
.action((options) => {
|
|
420
|
-
contextUpdate(options);
|
|
468
|
+
contextUpdate({ ...options, specId: options.spec });
|
|
421
469
|
});
|
|
422
470
|
|
|
423
471
|
contextCmd
|
|
424
472
|
.command("detail <section>")
|
|
425
473
|
.description("Mostra secao especifica do contexto (standards, decisions, patterns, knowledge, architecture)")
|
|
426
474
|
.option("--json", "Output JSON")
|
|
427
|
-
.
|
|
428
|
-
|
|
475
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
476
|
+
.action((section: string, opts: { json?: boolean; spec?: string }) => {
|
|
477
|
+
contextDetail(section, opts.json, opts.spec);
|
|
429
478
|
});
|
|
430
479
|
|
|
431
480
|
// Manter compatibilidade: context sem subcomando = export
|
|
@@ -433,8 +482,9 @@ program
|
|
|
433
482
|
.command("ctx")
|
|
434
483
|
.description("Alias para context export (compatibilidade)")
|
|
435
484
|
.option("--task <id>", "ID da task especifica")
|
|
485
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
436
486
|
.action((options) => {
|
|
437
|
-
contextExport({ ...options, json: true });
|
|
487
|
+
contextExport({ ...options, json: true, specId: options.spec });
|
|
438
488
|
});
|
|
439
489
|
|
|
440
490
|
program
|
|
@@ -500,7 +550,7 @@ discoverCmd
|
|
|
500
550
|
});
|
|
501
551
|
|
|
502
552
|
discoverCmd
|
|
503
|
-
.command("reset")
|
|
553
|
+
.command("reset", { hidden: true })
|
|
504
554
|
.description("Reseta descoberta para refazer")
|
|
505
555
|
.action(() => {
|
|
506
556
|
discoverReset();
|
|
@@ -568,7 +618,7 @@ discoverCmd
|
|
|
568
618
|
});
|
|
569
619
|
|
|
570
620
|
discoverCmd
|
|
571
|
-
.command("pattern-edit <name>")
|
|
621
|
+
.command("pattern-edit <name>", { hidden: true })
|
|
572
622
|
.description("Edita um implementation pattern")
|
|
573
623
|
.option("--category <cat>", "Nova categoria")
|
|
574
624
|
.option("--scope <scope>", "Novo escopo")
|
|
@@ -618,7 +668,7 @@ discoverCmd
|
|
|
618
668
|
});
|
|
619
669
|
|
|
620
670
|
discoverCmd
|
|
621
|
-
.command("analyze-file <path>")
|
|
671
|
+
.command("analyze-file <path>", { hidden: true })
|
|
622
672
|
.description("Analisa estrutura de um arquivo (imports, exports, convencoes)")
|
|
623
673
|
.option("--json", "Saida em JSON")
|
|
624
674
|
.action((path, options) => {
|
|
@@ -627,7 +677,7 @@ discoverCmd
|
|
|
627
677
|
|
|
628
678
|
// v9.0: Analise profunda com metadata enriquecida + grepai
|
|
629
679
|
discoverCmd
|
|
630
|
-
.command("analyze-deep <files...>")
|
|
680
|
+
.command("analyze-deep <files...>", { hidden: true })
|
|
631
681
|
.description("Analise profunda de arquivos (hooks, patterns, directives)")
|
|
632
682
|
.option("--json", "Saida em JSON")
|
|
633
683
|
.action((files, options) => {
|
|
@@ -637,7 +687,7 @@ discoverCmd
|
|
|
637
687
|
|
|
638
688
|
// v9.0: Anti-patterns de gate bypasses
|
|
639
689
|
discoverCmd
|
|
640
|
-
.command("extract-anti-patterns")
|
|
690
|
+
.command("extract-anti-patterns", { hidden: true })
|
|
641
691
|
.description("Extrai anti-patterns do historico de gate bypasses")
|
|
642
692
|
.action(() => {
|
|
643
693
|
const { extractAntiPatternsFromHistory } = require("./commands/patterns");
|
|
@@ -645,7 +695,7 @@ discoverCmd
|
|
|
645
695
|
});
|
|
646
696
|
|
|
647
697
|
discoverCmd
|
|
648
|
-
.command("export-patterns")
|
|
698
|
+
.command("export-patterns", { hidden: true })
|
|
649
699
|
.description("Regenera arquivo patterns.md")
|
|
650
700
|
.action(() => {
|
|
651
701
|
discoverExportPatterns();
|
|
@@ -863,11 +913,12 @@ program
|
|
|
863
913
|
.description("Limpa tasks/features mantendo configuracoes do projeto (standards, patterns, PRD)")
|
|
864
914
|
.option("--force", "Confirma a limpeza (sem isso apenas mostra o que sera removido)")
|
|
865
915
|
.option("--show", "Mostra estado atual dos arquivos do workflow")
|
|
916
|
+
.option("--spec <id>", "Limpa apenas o spec especificado")
|
|
866
917
|
.action((options) => {
|
|
867
918
|
if (options.show) {
|
|
868
919
|
clearShow();
|
|
869
920
|
} else {
|
|
870
|
-
clearTasks({ force: options.force });
|
|
921
|
+
clearTasks({ force: options.force, specId: options.spec });
|
|
871
922
|
}
|
|
872
923
|
});
|
|
873
924
|
|