@rehearsal-db/core 0.1.0-beta.4 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.md +11 -2
- package/SUPPORT.md +5 -0
- package/docs/commands.md +15 -4
- package/docs/getting-started.md +12 -4
- package/docs/sanitization.md +8 -5
- package/package.json +2 -2
- package/scripts/lib/rehearsal/baseline_preparation.mjs +138 -23
- package/scripts/lib/rehearsal/diagnostics.mjs +1 -1
- package/scripts/lib/rehearsal/input_discovery.mjs +155 -0
- package/scripts/lib/rehearsal/policy_review.mjs +45 -5
- package/scripts/lib/rehearsal/support_report.mjs +102 -0
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +393 -128
|
@@ -36,9 +36,11 @@ import {
|
|
|
36
36
|
import { createSyntheticBaselineFromFiles } from "../../lib/rehearsal/baseline_builder.mjs";
|
|
37
37
|
import {
|
|
38
38
|
applyBaselinePreparation,
|
|
39
|
+
inspectBaselineInputFiles,
|
|
39
40
|
planBaselinePreparation,
|
|
40
41
|
summarizeBaselinePreparation,
|
|
41
42
|
} from "../../lib/rehearsal/baseline_preparation.mjs";
|
|
43
|
+
import { discoverBaselineInputFiles } from "../../lib/rehearsal/input_discovery.mjs";
|
|
42
44
|
import {
|
|
43
45
|
applyRehearsalSetup,
|
|
44
46
|
inspectRehearsalNodeRuntime,
|
|
@@ -49,8 +51,11 @@ import { formatCount } from "../../lib/rehearsal/human_output.mjs";
|
|
|
49
51
|
import {
|
|
50
52
|
applyReviewedPolicy,
|
|
51
53
|
completePolicyDraft,
|
|
54
|
+
createSafeTablePreset,
|
|
52
55
|
readReviewablePolicyDraft,
|
|
56
|
+
suggestPolicyExceptionColumns,
|
|
53
57
|
} from "../../lib/rehearsal/policy_review.mjs";
|
|
58
|
+
import { collectRehearsalSupportReport } from "../../lib/rehearsal/support_report.mjs";
|
|
54
59
|
|
|
55
60
|
const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
|
|
56
61
|
const projectRoot = process.cwd();
|
|
@@ -273,6 +278,7 @@ const promptForChoice = async ({ message, options, flags = {} }) => {
|
|
|
273
278
|
if (useStyledPrompts(flags)) {
|
|
274
279
|
const selected = await prompts.select({
|
|
275
280
|
message,
|
|
281
|
+
maxItems: options.length,
|
|
276
282
|
options: options.map((option) => ({
|
|
277
283
|
value: option,
|
|
278
284
|
label: option.label,
|
|
@@ -341,7 +347,7 @@ const promptForPath = async ({
|
|
|
341
347
|
placeholder: defaultValue ?? (optional ? "Leave blank for none" : ""),
|
|
342
348
|
defaultValue,
|
|
343
349
|
validate: (value) => {
|
|
344
|
-
const resolved = String(value
|
|
350
|
+
const resolved = String(value || defaultValue || "").trim();
|
|
345
351
|
if (!optional && !resolved) return "Enter a value.";
|
|
346
352
|
return resolved && validate ? validate(resolved) : undefined;
|
|
347
353
|
},
|
|
@@ -375,16 +381,107 @@ const promptForPath = async ({
|
|
|
375
381
|
}
|
|
376
382
|
};
|
|
377
383
|
|
|
384
|
+
const promptForDiscoveredPath = async ({
|
|
385
|
+
message,
|
|
386
|
+
candidates,
|
|
387
|
+
optional = false,
|
|
388
|
+
flags,
|
|
389
|
+
}) => {
|
|
390
|
+
if (candidates.length === 0) {
|
|
391
|
+
return promptForPath({ message, optional, flags });
|
|
392
|
+
}
|
|
393
|
+
const noneValue = "\0none";
|
|
394
|
+
const manualValue = "\0manual";
|
|
395
|
+
const selected = await promptForSelection({
|
|
396
|
+
message,
|
|
397
|
+
flags,
|
|
398
|
+
options: [
|
|
399
|
+
...(optional
|
|
400
|
+
? [
|
|
401
|
+
{
|
|
402
|
+
label: "No storage assets",
|
|
403
|
+
hint: "Continue without an asset manifest",
|
|
404
|
+
value: noneValue,
|
|
405
|
+
},
|
|
406
|
+
]
|
|
407
|
+
: []),
|
|
408
|
+
...candidates.map((path) => ({ label: path, value: path })),
|
|
409
|
+
{ label: "Enter another path", value: manualValue },
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
if (!selected || selected === noneValue) return undefined;
|
|
413
|
+
if (selected !== manualValue) return selected;
|
|
414
|
+
return promptForPath({ message, optional, flags });
|
|
415
|
+
};
|
|
416
|
+
|
|
378
417
|
const promptForSelection = async ({ message, options, flags }) => {
|
|
379
418
|
const selected = await promptForChoice({ message, options, flags });
|
|
380
419
|
return selected?.value;
|
|
381
420
|
};
|
|
382
421
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
422
|
+
const promptForMultipleChoice = async ({
|
|
423
|
+
message,
|
|
424
|
+
options,
|
|
425
|
+
initialValues = [],
|
|
426
|
+
flags,
|
|
427
|
+
}) => {
|
|
428
|
+
if (useStyledPrompts(flags)) {
|
|
429
|
+
const selected = await prompts.multiselect({
|
|
430
|
+
message,
|
|
431
|
+
options: options.map((option) => ({
|
|
432
|
+
value: option.value,
|
|
433
|
+
label: option.label,
|
|
434
|
+
hint: option.hint,
|
|
435
|
+
})),
|
|
436
|
+
initialValues,
|
|
437
|
+
required: false,
|
|
438
|
+
});
|
|
439
|
+
return prompts.isCancel(selected) ? undefined : selected;
|
|
440
|
+
}
|
|
441
|
+
const initial = new Set(initialValues);
|
|
442
|
+
console.log("");
|
|
443
|
+
console.log(message);
|
|
444
|
+
for (const [index, option] of options.entries()) {
|
|
445
|
+
console.log(
|
|
446
|
+
` ${index + 1}. ${option.label}${initial.has(option.value) ? " (suggested)" : ""}`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const defaultNumbers = options
|
|
450
|
+
.map((option, index) => (initial.has(option.value) ? index + 1 : undefined))
|
|
451
|
+
.filter(Boolean)
|
|
452
|
+
.join(",");
|
|
453
|
+
const prompt = createInterface({
|
|
454
|
+
input: process.stdin,
|
|
455
|
+
output: process.stdout,
|
|
456
|
+
});
|
|
457
|
+
try {
|
|
458
|
+
while (true) {
|
|
459
|
+
const suffix = defaultNumbers ? ` [${defaultNumbers}]` : " [none]";
|
|
460
|
+
const answer = (
|
|
461
|
+
await prompt.question(
|
|
462
|
+
`Choose comma-separated numbers, all, or none${suffix}: `,
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
.trim()
|
|
466
|
+
.toLowerCase();
|
|
467
|
+
if (!answer) return [...initialValues];
|
|
468
|
+
if (answer === "none") return [];
|
|
469
|
+
if (answer === "all") return options.map((option) => option.value);
|
|
470
|
+
const indexes = answer.split(",").map((value) => Number(value.trim()));
|
|
471
|
+
if (
|
|
472
|
+
indexes.length > 0 &&
|
|
473
|
+
indexes.every(
|
|
474
|
+
(index) =>
|
|
475
|
+
Number.isInteger(index) && index >= 1 && index <= options.length,
|
|
476
|
+
)
|
|
477
|
+
) {
|
|
478
|
+
return [...new Set(indexes)].map((index) => options[index - 1].value);
|
|
479
|
+
}
|
|
480
|
+
console.log(`Choose numbers from 1 to ${options.length}, all, or none.`);
|
|
481
|
+
}
|
|
482
|
+
} finally {
|
|
483
|
+
prompt.close();
|
|
386
484
|
}
|
|
387
|
-
return undefined;
|
|
388
485
|
};
|
|
389
486
|
|
|
390
487
|
const inspectGuidedProject = async (planOptions) => {
|
|
@@ -467,116 +564,180 @@ const reviewPolicyInteractively = async ({ state, flags }) => {
|
|
|
467
564
|
(total, table) => total + (table.columns?.length ?? 0),
|
|
468
565
|
0,
|
|
469
566
|
);
|
|
470
|
-
|
|
567
|
+
const tableSummaries = [];
|
|
568
|
+
let individuallyReviewed = 0;
|
|
471
569
|
if (useStyledPrompts(flags)) {
|
|
472
570
|
prompts.note(
|
|
473
571
|
[
|
|
474
|
-
`${formatCount(totalColumns, "column")} require explicit review.`,
|
|
572
|
+
`${formatCount(totalColumns, "column")} across ${formatCount(draft.tables.length, "table")} require explicit review.`,
|
|
475
573
|
"No row values are displayed or changed by this step.",
|
|
476
|
-
"
|
|
574
|
+
"Safe defaults replace values, mark generated NEVER and identity NO, and set no foreign key.",
|
|
575
|
+
"Likely structural columns start selected for individual review.",
|
|
477
576
|
].join("\n"),
|
|
478
577
|
"Review the script",
|
|
479
578
|
);
|
|
480
579
|
}
|
|
580
|
+
const reviewColumn = async ({ table, column }) => {
|
|
581
|
+
individuallyReviewed += 1;
|
|
582
|
+
const label = `${table}.${column}`;
|
|
583
|
+
const action = await promptForSelection({
|
|
584
|
+
message: `${label}: how should this value be handled?`,
|
|
585
|
+
flags,
|
|
586
|
+
options: [
|
|
587
|
+
{
|
|
588
|
+
label: "Replace with synthetic data",
|
|
589
|
+
hint: "Safest default",
|
|
590
|
+
value: "REPLACE WITH SYNTHETIC",
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
label: "Pseudonymize consistently",
|
|
594
|
+
hint: "Stable links without original values",
|
|
595
|
+
value: "PSEUDONYMIZE",
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
label: "Derive a safe value",
|
|
599
|
+
value: "DERIVE",
|
|
600
|
+
},
|
|
601
|
+
{ label: "Exclude this field", value: "EXCLUDE" },
|
|
602
|
+
{
|
|
603
|
+
label: "Keep exactly",
|
|
604
|
+
hint: "Only for values explicitly safe to retain",
|
|
605
|
+
value: "KEEP EXACTLY",
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
});
|
|
609
|
+
if (!action) throw new Error("Policy review was cancelled.");
|
|
610
|
+
const generated = await promptForSelection({
|
|
611
|
+
message: `${label}: is this column always database-generated?`,
|
|
612
|
+
flags,
|
|
613
|
+
options: [
|
|
614
|
+
{ label: "No", value: "NEVER" },
|
|
615
|
+
{ label: "Yes", value: "ALWAYS" },
|
|
616
|
+
],
|
|
617
|
+
});
|
|
618
|
+
if (!generated) throw new Error("Policy review was cancelled.");
|
|
619
|
+
const identity = await promptForSelection({
|
|
620
|
+
message: `${label}: is this an identity column?`,
|
|
621
|
+
flags,
|
|
622
|
+
options: [
|
|
623
|
+
{ label: "No", value: "NO" },
|
|
624
|
+
{ label: "Yes", value: "YES" },
|
|
625
|
+
],
|
|
626
|
+
});
|
|
627
|
+
if (!identity) throw new Error("Policy review was cancelled.");
|
|
628
|
+
const hasForeignKey = await promptForConfirmation(
|
|
629
|
+
`${label}: does this column reference another table?`,
|
|
630
|
+
flags,
|
|
631
|
+
{ cancelValue: null },
|
|
632
|
+
);
|
|
633
|
+
if (hasForeignKey === null) {
|
|
634
|
+
throw new Error("Policy review was cancelled.");
|
|
635
|
+
}
|
|
636
|
+
let foreignKey = null;
|
|
637
|
+
if (hasForeignKey) {
|
|
638
|
+
const schema = await promptForPath({
|
|
639
|
+
message: `${label}: referenced schema`,
|
|
640
|
+
defaultValue: "public",
|
|
641
|
+
flags,
|
|
642
|
+
validate: (value) =>
|
|
643
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
644
|
+
? undefined
|
|
645
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
646
|
+
});
|
|
647
|
+
const referencedTable = await promptForPath({
|
|
648
|
+
message: `${label}: referenced table`,
|
|
649
|
+
flags,
|
|
650
|
+
validate: (value) =>
|
|
651
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
652
|
+
? undefined
|
|
653
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
654
|
+
});
|
|
655
|
+
const referencedColumn = await promptForPath({
|
|
656
|
+
message: `${label}: referenced column`,
|
|
657
|
+
flags,
|
|
658
|
+
validate: (value) =>
|
|
659
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
660
|
+
? undefined
|
|
661
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
662
|
+
});
|
|
663
|
+
if (!schema || !referencedTable || !referencedColumn) {
|
|
664
|
+
throw new Error("Policy review was cancelled.");
|
|
665
|
+
}
|
|
666
|
+
foreignKey = {
|
|
667
|
+
schema,
|
|
668
|
+
table: referencedTable,
|
|
669
|
+
column: referencedColumn,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return { action, generated, identity, foreignKey };
|
|
673
|
+
};
|
|
481
674
|
const policy = await completePolicyDraft({
|
|
482
675
|
draft,
|
|
483
|
-
reviewColumn
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
676
|
+
reviewColumn,
|
|
677
|
+
reviewTable: async ({ table, columns }) => {
|
|
678
|
+
if (columns.length === 1) {
|
|
679
|
+
tableSummaries.push({ table, defaulted: 0, individual: 1 });
|
|
680
|
+
return {};
|
|
681
|
+
}
|
|
682
|
+
const mode = await promptForSelection({
|
|
683
|
+
message: `${table}: how would you like to review ${formatCount(columns.length, "column")}?`,
|
|
488
684
|
flags,
|
|
489
685
|
options: [
|
|
490
686
|
{
|
|
491
|
-
label: "
|
|
492
|
-
hint: "
|
|
493
|
-
value: "
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
label: "Pseudonymize consistently",
|
|
497
|
-
hint: "Stable links without original values",
|
|
498
|
-
value: "PSEUDONYMIZE",
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
label: "Derive a safe value",
|
|
502
|
-
value: "DERIVE",
|
|
687
|
+
label: "Use safe defaults, then review exceptions",
|
|
688
|
+
hint: "Best for larger tables",
|
|
689
|
+
value: "preset",
|
|
503
690
|
},
|
|
504
|
-
{ label: "Exclude this field", value: "EXCLUDE" },
|
|
505
691
|
{
|
|
506
|
-
label: "
|
|
507
|
-
|
|
508
|
-
value: "KEEP EXACTLY",
|
|
692
|
+
label: "Review every column individually",
|
|
693
|
+
value: "individual",
|
|
509
694
|
},
|
|
510
695
|
],
|
|
511
696
|
});
|
|
512
|
-
if (!
|
|
513
|
-
|
|
514
|
-
|
|
697
|
+
if (!mode) throw new Error("Policy review was cancelled.");
|
|
698
|
+
if (mode === "individual") {
|
|
699
|
+
tableSummaries.push({
|
|
700
|
+
table,
|
|
701
|
+
defaulted: 0,
|
|
702
|
+
individual: columns.length,
|
|
703
|
+
});
|
|
704
|
+
return {};
|
|
705
|
+
}
|
|
706
|
+
const suggested = suggestPolicyExceptionColumns(columns);
|
|
707
|
+
const exceptions = await promptForMultipleChoice({
|
|
708
|
+
message: `${table}: select columns that need individual review`,
|
|
515
709
|
flags,
|
|
516
|
-
options:
|
|
517
|
-
|
|
518
|
-
{ label: "Yes", value: "ALWAYS" },
|
|
519
|
-
],
|
|
710
|
+
options: columns.map((column) => ({ label: column, value: column })),
|
|
711
|
+
initialValues: suggested,
|
|
520
712
|
});
|
|
521
|
-
if (!
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
],
|
|
713
|
+
if (!exceptions) throw new Error("Policy review was cancelled.");
|
|
714
|
+
const tableDecisions = createSafeTablePreset(columns);
|
|
715
|
+
for (const column of exceptions) delete tableDecisions[column];
|
|
716
|
+
tableSummaries.push({
|
|
717
|
+
table,
|
|
718
|
+
defaulted: columns.length - exceptions.length,
|
|
719
|
+
individual: exceptions.length,
|
|
529
720
|
});
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
{ cancelValue: null },
|
|
535
|
-
);
|
|
536
|
-
if (hasForeignKey === null) {
|
|
537
|
-
throw new Error("Policy review was cancelled.");
|
|
538
|
-
}
|
|
539
|
-
let foreignKey = null;
|
|
540
|
-
if (hasForeignKey) {
|
|
541
|
-
const schema = await promptForPath({
|
|
542
|
-
message: `${label}: referenced schema`,
|
|
543
|
-
defaultValue: "public",
|
|
544
|
-
flags,
|
|
545
|
-
validate: (value) =>
|
|
546
|
-
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
547
|
-
? undefined
|
|
548
|
-
: "Use a lowercase PostgreSQL identifier.",
|
|
549
|
-
});
|
|
550
|
-
const referencedTable = await promptForPath({
|
|
551
|
-
message: `${label}: referenced table`,
|
|
552
|
-
flags,
|
|
553
|
-
validate: (value) =>
|
|
554
|
-
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
555
|
-
? undefined
|
|
556
|
-
: "Use a lowercase PostgreSQL identifier.",
|
|
557
|
-
});
|
|
558
|
-
const referencedColumn = await promptForPath({
|
|
559
|
-
message: `${label}: referenced column`,
|
|
560
|
-
flags,
|
|
561
|
-
validate: (value) =>
|
|
562
|
-
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
563
|
-
? undefined
|
|
564
|
-
: "Use a lowercase PostgreSQL identifier.",
|
|
565
|
-
});
|
|
566
|
-
if (!schema || !referencedTable || !referencedColumn) {
|
|
567
|
-
throw new Error("Policy review was cancelled.");
|
|
568
|
-
}
|
|
569
|
-
foreignKey = {
|
|
570
|
-
schema,
|
|
571
|
-
table: referencedTable,
|
|
572
|
-
column: referencedColumn,
|
|
573
|
-
};
|
|
721
|
+
if (useStyledPrompts(flags)) {
|
|
722
|
+
prompts.log.info(
|
|
723
|
+
`${table}: ${formatCount(columns.length - exceptions.length, "safe default")}; ${formatCount(exceptions.length, "exception")}.`,
|
|
724
|
+
);
|
|
574
725
|
}
|
|
575
|
-
return
|
|
726
|
+
return tableDecisions;
|
|
576
727
|
},
|
|
577
728
|
});
|
|
729
|
+
const defaultedColumns = totalColumns - individuallyReviewed;
|
|
730
|
+
const summaryLines = tableSummaries.map(
|
|
731
|
+
(summary) =>
|
|
732
|
+
`${summary.table}: ${formatCount(summary.defaulted, "safe default")}; ${formatCount(summary.individual, "individual review")}`,
|
|
733
|
+
);
|
|
734
|
+
if (useStyledPrompts(flags)) {
|
|
735
|
+
prompts.note(summaryLines.join("\n"), "Review summary");
|
|
736
|
+
} else {
|
|
737
|
+
console.log(["", "Review summary", ...summaryLines, ""].join("\n"));
|
|
738
|
+
}
|
|
578
739
|
const accepted = await promptForConfirmation(
|
|
579
|
-
`Save ${formatCount(totalColumns, "
|
|
740
|
+
`Save ${formatCount(totalColumns, "classified column")} and activate this policy?`,
|
|
580
741
|
flags,
|
|
581
742
|
);
|
|
582
743
|
if (!accepted) return false;
|
|
@@ -585,14 +746,11 @@ const reviewPolicyInteractively = async ({ state, flags }) => {
|
|
|
585
746
|
originalSource: source,
|
|
586
747
|
policy,
|
|
587
748
|
});
|
|
749
|
+
const result = `${formatCount(totalColumns, "column")} classified (${formatCount(defaultedColumns, "safe default")}; ${formatCount(individuallyReviewed, "individual review")}).`;
|
|
588
750
|
if (useStyledPrompts(flags)) {
|
|
589
|
-
prompts.log.success(
|
|
590
|
-
`${formatCount(totalColumns, "column")} reviewed; the policy is ready for baseline creation.`,
|
|
591
|
-
);
|
|
751
|
+
prompts.log.success(`${result} The policy is ready for baseline creation.`);
|
|
592
752
|
} else {
|
|
593
|
-
console.log(
|
|
594
|
-
`Reviewed policy saved: ${formatCount(totalColumns, "column")} classified.`,
|
|
595
|
-
);
|
|
753
|
+
console.log(`Reviewed policy saved: ${result}`);
|
|
596
754
|
}
|
|
597
755
|
return true;
|
|
598
756
|
};
|
|
@@ -632,7 +790,7 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
632
790
|
} else if (state.policy === "needs-review") {
|
|
633
791
|
options.push({
|
|
634
792
|
label: "Review the script",
|
|
635
|
-
hint: "
|
|
793
|
+
hint: "Apply table presets and review exceptions",
|
|
636
794
|
command: "policy-review",
|
|
637
795
|
});
|
|
638
796
|
} else {
|
|
@@ -660,6 +818,11 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
660
818
|
});
|
|
661
819
|
}
|
|
662
820
|
options.push(
|
|
821
|
+
{
|
|
822
|
+
label: "Get help",
|
|
823
|
+
hint: "Create a safe, copy-ready support report",
|
|
824
|
+
command: "support",
|
|
825
|
+
},
|
|
663
826
|
{ label: "Show all commands", command: "help" },
|
|
664
827
|
{ label: "Exit", command: "exit" },
|
|
665
828
|
);
|
|
@@ -797,55 +960,95 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
797
960
|
"",
|
|
798
961
|
].join("\n"),
|
|
799
962
|
);
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
963
|
+
const discovered = await discoverBaselineInputFiles({ projectRoot });
|
|
964
|
+
const detectedLines = [
|
|
965
|
+
discovered.records.length
|
|
966
|
+
? `Records: ${discovered.records.join(", ")}`
|
|
967
|
+
: "Records: none detected",
|
|
968
|
+
discovered.ledgers.length
|
|
969
|
+
? `Migration ledgers: ${discovered.ledgers.join(", ")}`
|
|
970
|
+
: "Migration ledgers: none detected",
|
|
971
|
+
discovered.assetManifests.length
|
|
972
|
+
? `Storage manifests: ${discovered.assetManifests.join(", ")}`
|
|
973
|
+
: "Storage manifests: none detected",
|
|
974
|
+
"Only paths and file structure were inspected; row values stay hidden.",
|
|
975
|
+
];
|
|
976
|
+
if (useStyledPrompts(flags)) {
|
|
977
|
+
prompts.note(detectedLines.join("\n"), "Detected safe local inputs");
|
|
978
|
+
} else {
|
|
979
|
+
console.log(
|
|
980
|
+
["Detected safe local inputs", ...detectedLines, ""].join("\n"),
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
flags.recordsPath = await promptForDiscoveredPath({
|
|
808
984
|
message: "Sanitized NDJSON records",
|
|
809
|
-
|
|
985
|
+
candidates: discovered.records,
|
|
810
986
|
flags,
|
|
811
987
|
});
|
|
812
|
-
flags.ledgerPath = await
|
|
988
|
+
flags.ledgerPath = await promptForDiscoveredPath({
|
|
813
989
|
message: "Migration ledger",
|
|
814
|
-
|
|
990
|
+
candidates: discovered.ledgers,
|
|
815
991
|
flags,
|
|
816
992
|
});
|
|
817
|
-
|
|
818
|
-
|
|
993
|
+
try {
|
|
994
|
+
if (selected.command === "baseline-prepare") {
|
|
995
|
+
const preparationPlan = await planBaselinePreparation({
|
|
996
|
+
...planOptions,
|
|
997
|
+
recordsPath: flags.recordsPath,
|
|
998
|
+
ledgerPath: flags.ledgerPath,
|
|
999
|
+
});
|
|
1000
|
+
const preview = {
|
|
1001
|
+
...summarizeBaselinePreparation(preparationPlan, {
|
|
1002
|
+
mode: "preview",
|
|
1003
|
+
}),
|
|
1004
|
+
nextAction:
|
|
1005
|
+
"Review this schema-only summary. No file is written unless you approve below.",
|
|
1006
|
+
};
|
|
1007
|
+
console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
|
|
1008
|
+
const accepted = await promptForConfirmation(
|
|
1009
|
+
`Create the REVIEW REQUIRED draft at ${preview.destination}?`,
|
|
1010
|
+
flags,
|
|
1011
|
+
);
|
|
1012
|
+
if (!accepted) {
|
|
1013
|
+
console.log("No changes made.");
|
|
1014
|
+
return "refresh";
|
|
1015
|
+
}
|
|
1016
|
+
flags.preparationPlan = preparationPlan;
|
|
1017
|
+
flags.write = true;
|
|
1018
|
+
return "baseline prepare";
|
|
1019
|
+
}
|
|
1020
|
+
flags.assetsPath = await promptForDiscoveredPath({
|
|
1021
|
+
message: "Storage assets",
|
|
1022
|
+
candidates: discovered.assetManifests,
|
|
1023
|
+
optional: true,
|
|
1024
|
+
flags,
|
|
1025
|
+
});
|
|
1026
|
+
const inspection = await inspectBaselineInputFiles({
|
|
819
1027
|
...planOptions,
|
|
820
1028
|
recordsPath: flags.recordsPath,
|
|
821
1029
|
ledgerPath: flags.ledgerPath,
|
|
1030
|
+
assetsPath: flags.assetsPath,
|
|
822
1031
|
});
|
|
823
|
-
|
|
824
|
-
...summarizeBaselinePreparation(preparationPlan, {
|
|
825
|
-
mode: "preview",
|
|
826
|
-
}),
|
|
827
|
-
nextAction:
|
|
828
|
-
"Review this schema-only summary. No file is written unless you approve below.",
|
|
829
|
-
};
|
|
830
|
-
console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
|
|
1032
|
+
console.log(`\n${renderBaselineInputInspection(inspection, flags)}\n`);
|
|
831
1033
|
const accepted = await promptForConfirmation(
|
|
832
|
-
|
|
1034
|
+
"Create and activate this immutable local baseline?",
|
|
833
1035
|
flags,
|
|
834
1036
|
);
|
|
835
1037
|
if (!accepted) {
|
|
836
1038
|
console.log("No changes made.");
|
|
837
1039
|
return "refresh";
|
|
838
1040
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1041
|
+
return "baseline create";
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
const message = redactDiagnosticValue(String(error?.message ?? error));
|
|
1044
|
+
if (useStyledPrompts(flags)) {
|
|
1045
|
+
prompts.cancel("These baseline inputs need attention.");
|
|
1046
|
+
prompts.note(message, "How to continue");
|
|
1047
|
+
} else {
|
|
1048
|
+
console.log(`\nBASELINE INPUTS NEED ATTENTION\n${message}\n`);
|
|
1049
|
+
}
|
|
1050
|
+
return "refresh";
|
|
842
1051
|
}
|
|
843
|
-
flags.assetsPath = await promptForPath({
|
|
844
|
-
message: "Storage asset manifest (optional)",
|
|
845
|
-
optional: true,
|
|
846
|
-
flags,
|
|
847
|
-
});
|
|
848
|
-
return "baseline create";
|
|
849
1052
|
}
|
|
850
1053
|
if (selected.command !== "setup-write") return selected.command;
|
|
851
1054
|
|
|
@@ -1314,6 +1517,56 @@ const renderBaselinePreparation = (result, flags = {}) =>
|
|
|
1314
1517
|
result.nextAction,
|
|
1315
1518
|
].join("\n");
|
|
1316
1519
|
|
|
1520
|
+
const renderBaselineInputInspection = (result, flags = {}) =>
|
|
1521
|
+
[
|
|
1522
|
+
"BASELINE INPUTS — READY",
|
|
1523
|
+
`Records: ${formatCount(result.rowCount, "row")} across ${formatCount(result.tables.length, "table")} from ${result.recordsPath}`,
|
|
1524
|
+
`Migrations: ${formatCount(result.migrationCount, "migration")} through ${result.migrationCutoff} from ${result.ledgerPath}`,
|
|
1525
|
+
result.assetsPath
|
|
1526
|
+
? `Storage: ${formatCount(result.assetCount, "asset")} from ${result.assetsPath}`
|
|
1527
|
+
: "Storage: no asset manifest selected",
|
|
1528
|
+
"",
|
|
1529
|
+
"Detected tables (values are never printed)",
|
|
1530
|
+
...result.tables.map(
|
|
1531
|
+
(table) =>
|
|
1532
|
+
` ${terminalStyle(flags, "36", "→")} ${table.name}: ${formatCount(table.rowCount, "row")}`,
|
|
1533
|
+
),
|
|
1534
|
+
"",
|
|
1535
|
+
"All inputs are project-local regular files and passed structural validation.",
|
|
1536
|
+
].join("\n");
|
|
1537
|
+
|
|
1538
|
+
const renderSupportReport = (report) => {
|
|
1539
|
+
const toolLine = (label, tool) =>
|
|
1540
|
+
`${label}: ${tool.status === "available" ? tool.version : tool.status.replaceAll("_", " ")}`;
|
|
1541
|
+
return [
|
|
1542
|
+
"REHEARSAL SUPPORT REPORT",
|
|
1543
|
+
"",
|
|
1544
|
+
`Rehearsal: ${report.rehearsalVersion}`,
|
|
1545
|
+
`System: ${report.system.platform} ${report.system.architecture} (${report.system.release})`,
|
|
1546
|
+
`Node.js: ${report.system.node}`,
|
|
1547
|
+
toolLine("npm", report.tools.npm),
|
|
1548
|
+
toolLine("Supabase CLI", report.tools.supabase),
|
|
1549
|
+
toolLine("Docker client", report.tools.dockerClient),
|
|
1550
|
+
toolLine("Docker server", report.tools.dockerServer),
|
|
1551
|
+
"",
|
|
1552
|
+
`Readiness: ${report.readiness.state}`,
|
|
1553
|
+
...report.readiness.checks.map((check) =>
|
|
1554
|
+
check.status === "pass"
|
|
1555
|
+
? ` ✓ ${check.label}`
|
|
1556
|
+
: ` ✗ ${check.label}${check.remediation ? ` — ${check.remediation}` : ""}`,
|
|
1557
|
+
),
|
|
1558
|
+
...(report.readiness.quarantinedHostedVariableCount
|
|
1559
|
+
? [
|
|
1560
|
+
` ! ${formatCount(report.readiness.quarantinedHostedVariableCount, "ambient hosted variable")} detected and quarantined`,
|
|
1561
|
+
]
|
|
1562
|
+
: []),
|
|
1563
|
+
"",
|
|
1564
|
+
"Privacy: no row values, credentials, project paths, migration SQL, or baseline identifiers are included.",
|
|
1565
|
+
"Review this report before sharing it.",
|
|
1566
|
+
`Report a reproducible issue: ${report.supportUrl}`,
|
|
1567
|
+
].join("\n");
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1317
1570
|
const usage = () => `Usage: rehearsal <command> [options]
|
|
1318
1571
|
|
|
1319
1572
|
Commands:
|
|
@@ -1323,6 +1576,7 @@ Commands:
|
|
|
1323
1576
|
baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
|
|
1324
1577
|
baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
|
|
1325
1578
|
doctor Check whether Rehearsal is safe and ready
|
|
1579
|
+
support Print a safe, copy-ready support report
|
|
1326
1580
|
explain Show the immutable execution plan
|
|
1327
1581
|
run --dry-run Alias the exact explain plan without mutations
|
|
1328
1582
|
run --confirm-candidates= Execute reset, migration, and verification locally
|
|
@@ -1365,6 +1619,12 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
|
1365
1619
|
return;
|
|
1366
1620
|
}
|
|
1367
1621
|
if (command === "baseline create") {
|
|
1622
|
+
await inspectBaselineInputFiles({
|
|
1623
|
+
...planOptions,
|
|
1624
|
+
recordsPath: flags.recordsPath,
|
|
1625
|
+
ledgerPath: flags.ledgerPath,
|
|
1626
|
+
assetsPath: flags.assetsPath,
|
|
1627
|
+
});
|
|
1368
1628
|
const data = await createSyntheticBaselineFromFiles({
|
|
1369
1629
|
...planOptions,
|
|
1370
1630
|
recordsPath: flags.recordsPath,
|
|
@@ -1398,6 +1658,11 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
|
1398
1658
|
if (data.state !== "READY" && !guided) process.exitCode = 1;
|
|
1399
1659
|
return;
|
|
1400
1660
|
}
|
|
1661
|
+
if (command === "support") {
|
|
1662
|
+
const data = await collectRehearsalSupportReport(planOptions);
|
|
1663
|
+
emit({ command, data, flags, render: renderSupportReport });
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1401
1666
|
if (command === "explain" || (command === "run" && flags.dryRun)) {
|
|
1402
1667
|
const data = await buildRehearsalPlan(planOptions);
|
|
1403
1668
|
emit({ command, data, flags, render: renderPlan });
|