@praxisui/cron-builder 8.0.0-beta.0 → 8.0.0-beta.11

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/README.md CHANGED
@@ -79,6 +79,7 @@ Template-only example:
79
79
  Exports:
80
80
  - `PdxCronBuilderComponent`
81
81
  - Types: `CronBuilderMetadata`, `SimpleCronFormValue`, `AdvancedCronFormValue`, `CronPresetType`
82
+ - Scheduler authoring foundation: `ScheduleAuthoringConfig`, `CronDialect`, `CRON_DIALECTS`, `normalizeScheduleValue`, `compileScheduleExpression`, `validateScheduleAuthoringConfig`, `createSchedulePreview`
82
83
 
83
84
  Inputs/Outputs:
84
85
  - `metadata: CronBuilderMetadata` – configure fields, timezone, locale, presets, preview.
@@ -93,6 +94,61 @@ Inputs/Outputs:
93
94
  - `previewOccurrences?: number` – number of preview dates to show.
94
95
  - `validators?: { invalidCronMessage?: string }` – customize error messages.
95
96
 
97
+ ## Scheduler Authoring Foundation
98
+
99
+ `@praxisui/cron-builder` also exposes the canonical foundation for evolving CRON editing into schedule authoring.
100
+ This keeps enterprise scheduling semantics in the library instead of pushing cron parsing, dialect checks or preview policy into host applications.
101
+
102
+ The initial contract is `ScheduleAuthoringConfig`. It models:
103
+ - schedule kind: `once`, `interval`, `daily`, `weekly`, `monthly` or `customCron`;
104
+ - timezone and locale;
105
+ - CRON expression plus explicit dialect;
106
+ - recurrence intent;
107
+ - start/end window;
108
+ - execution policy such as enabled state, missed-run policy, concurrency and jitter;
109
+ - preview configuration;
110
+ - governance metadata such as name, owner and tags.
111
+
112
+ The exported `CRON_DIALECTS` matrix describes the supported dialect families:
113
+ - Unix cron
114
+ - Quartz cron
115
+ - AWS EventBridge cron
116
+ - Kubernetes CronJob
117
+ - GitHub Actions schedule
118
+ - Google Cloud Scheduler
119
+
120
+ Use `normalizeScheduleValue(value)` to convert legacy string values into `ScheduleAuthoringConfig` while preserving current `ControlValueAccessor` compatibility.
121
+ The current component still emits the existing CRON string; the schedule authoring contract is the platform foundation for the next visual authoring iteration.
122
+
123
+ Runtime helpers:
124
+ - `compileScheduleExpression(config)` compiles supported schedule intents (`interval`, `daily`, `weekly`, `monthly`, `customCron`) into CRON when the intent has a portable representation.
125
+ - `validateScheduleAuthoringConfig(config)` returns structured diagnostics for invalid or non-portable schedules.
126
+ - `createSchedulePreview(value, preview)` returns the compiled expression, humanized text, structured occurrences and diagnostics.
127
+
128
+ The AI adapter exposes both the legacy `value` and the structured scheduler state:
129
+ - `schedule` is the canonical authoring config snapshot.
130
+ - `diagnostics` contains read-only validation results.
131
+ - `preview` contains read-only structured next occurrences.
132
+
133
+ AI patches should prefer `schedule.kind` plus `schedule.recurrence` for business scheduling intent, and reserve `schedule.expression.cron` for explicit custom CRON requests.
134
+
135
+ ## Agentic Authoring
136
+
137
+ The executable authoring contract is exported as `PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST`.
138
+ It models typed operations over the same schedule runtime used by the component:
139
+
140
+ - `cron.expression.set`
141
+ - `cron.frequency.set`
142
+ - `cron.timezone.set`
143
+ - `cron.preset.apply`
144
+ - `cron.validate`
145
+ - `cron.preview.generate`
146
+
147
+ Expression, frequency, timezone and preset operations may patch canonical
148
+ `schedule` or compatibility `value` paths only after validation succeeds.
149
+ `cron.validate` and `cron.preview.generate` are read-only operations: invalid
150
+ schedules return structured `diagnostics` and do not mutate the current schedule.
151
+
96
152
  ## Build Notes
97
153
 
98
154
  The builder resolves validation, humanized descriptions and occurrence preview inside the library runtime.
@@ -429,6 +429,451 @@ function getNextCronOccurrences(expression, count, options) {
429
429
  return results;
430
430
  }
431
431
 
432
+ const SCHEDULE_AUTHORING_CONFIG_VERSION = 'v1';
433
+ const CRON_DIALECTS = {
434
+ unix: {
435
+ dialect: 'unix',
436
+ label: 'Unix cron',
437
+ fieldCount: { min: 5, max: 5 },
438
+ supportsSeconds: false,
439
+ supportsYear: false,
440
+ supportsNamedMonths: true,
441
+ supportsNamedWeekdays: true,
442
+ supportsQuestionMark: false,
443
+ supportsLast: false,
444
+ supportsNearestWeekday: false,
445
+ supportsNthWeekday: false,
446
+ timezoneMode: 'host-local',
447
+ dayOfMonthDayOfWeekRule: 'unix-or',
448
+ examples: ['*/5 * * * *', '0 9 * * 1-5'],
449
+ },
450
+ quartz: {
451
+ dialect: 'quartz',
452
+ label: 'Quartz cron',
453
+ fieldCount: { min: 6, max: 7 },
454
+ supportsSeconds: true,
455
+ supportsYear: true,
456
+ supportsNamedMonths: true,
457
+ supportsNamedWeekdays: true,
458
+ supportsQuestionMark: true,
459
+ supportsLast: true,
460
+ supportsNearestWeekday: true,
461
+ supportsNthWeekday: true,
462
+ timezoneMode: 'external-field',
463
+ dayOfMonthDayOfWeekRule: 'either-or-question-mark',
464
+ examples: ['0 0 9 ? * MON-FRI', '0 0 9 ? * 2#1'],
465
+ },
466
+ 'aws-eventbridge': {
467
+ dialect: 'aws-eventbridge',
468
+ label: 'AWS EventBridge cron',
469
+ fieldCount: { min: 6, max: 6 },
470
+ supportsSeconds: false,
471
+ supportsYear: true,
472
+ supportsNamedMonths: true,
473
+ supportsNamedWeekdays: true,
474
+ supportsQuestionMark: true,
475
+ supportsLast: true,
476
+ supportsNearestWeekday: false,
477
+ supportsNthWeekday: true,
478
+ timezoneMode: 'external-field',
479
+ dayOfMonthDayOfWeekRule: 'either-or-question-mark',
480
+ examples: ['0 9 ? * MON-FRI *', '0 9 ? * 2#1 *'],
481
+ },
482
+ kubernetes: {
483
+ dialect: 'kubernetes',
484
+ label: 'Kubernetes CronJob',
485
+ fieldCount: { min: 5, max: 5 },
486
+ supportsSeconds: false,
487
+ supportsYear: false,
488
+ supportsNamedMonths: true,
489
+ supportsNamedWeekdays: true,
490
+ supportsQuestionMark: true,
491
+ supportsLast: false,
492
+ supportsNearestWeekday: false,
493
+ supportsNthWeekday: false,
494
+ timezoneMode: 'external-field',
495
+ dayOfMonthDayOfWeekRule: 'unix-or',
496
+ examples: ['*/5 * * * *', '0 9 * * 1-5'],
497
+ },
498
+ 'github-actions': {
499
+ dialect: 'github-actions',
500
+ label: 'GitHub Actions schedule',
501
+ fieldCount: { min: 5, max: 5 },
502
+ supportsSeconds: false,
503
+ supportsYear: false,
504
+ supportsNamedMonths: true,
505
+ supportsNamedWeekdays: true,
506
+ supportsQuestionMark: false,
507
+ supportsLast: false,
508
+ supportsNearestWeekday: false,
509
+ supportsNthWeekday: false,
510
+ timezoneMode: 'fixed-utc',
511
+ dayOfMonthDayOfWeekRule: 'unix-or',
512
+ examples: ['*/15 * * * *', '0 9 * * 1-5'],
513
+ },
514
+ 'gcp-scheduler': {
515
+ dialect: 'gcp-scheduler',
516
+ label: 'Google Cloud Scheduler',
517
+ fieldCount: { min: 5, max: 5 },
518
+ supportsSeconds: false,
519
+ supportsYear: false,
520
+ supportsNamedMonths: true,
521
+ supportsNamedWeekdays: true,
522
+ supportsQuestionMark: false,
523
+ supportsLast: false,
524
+ supportsNearestWeekday: false,
525
+ supportsNthWeekday: false,
526
+ timezoneMode: 'external-field',
527
+ dayOfMonthDayOfWeekRule: 'unix-or',
528
+ examples: ['*/5 * * * *', '0 9 * * MON-FRI'],
529
+ },
530
+ };
531
+ function getCronDialectDefinition(dialect) {
532
+ return CRON_DIALECTS[dialect];
533
+ }
534
+
535
+ function normalizeScheduleValue(value, options = {}) {
536
+ if (value == null || value === '') {
537
+ return {
538
+ config: null,
539
+ diagnostics: [],
540
+ };
541
+ }
542
+ if (typeof value === 'string') {
543
+ return normalizeCronString(value, options);
544
+ }
545
+ return normalizeScheduleConfig(value, options);
546
+ }
547
+ function inferCronDialect(expression) {
548
+ const fieldCount = expression.trim().split(/\s+/).filter(Boolean).length;
549
+ return fieldCount === 6 ? 'quartz' : 'unix';
550
+ }
551
+ function normalizeCronString(cron, options) {
552
+ const trimmed = cron.trim();
553
+ const dialect = options.dialect ?? inferCronDialect(trimmed);
554
+ const diagnostics = createCronDiagnostics(trimmed, dialect);
555
+ return {
556
+ config: {
557
+ version: SCHEDULE_AUTHORING_CONFIG_VERSION,
558
+ kind: 'customCron',
559
+ timezone: options.timezone ?? 'UTC',
560
+ locale: options.locale,
561
+ expression: {
562
+ cron: trimmed,
563
+ dialect,
564
+ seconds: trimmed.split(/\s+/).length === 6 && dialect !== 'aws-eventbridge',
565
+ },
566
+ preview: {
567
+ occurrences: options.previewOccurrences ?? 5,
568
+ },
569
+ },
570
+ diagnostics,
571
+ };
572
+ }
573
+ function normalizeScheduleConfig(config, options) {
574
+ const normalized = {
575
+ ...config,
576
+ version: SCHEDULE_AUTHORING_CONFIG_VERSION,
577
+ timezone: config.timezone || options.timezone || 'UTC',
578
+ locale: config.locale ?? options.locale,
579
+ preview: {
580
+ occurrences: options.previewOccurrences ?? config.preview?.occurrences ?? 5,
581
+ from: config.preview?.from,
582
+ },
583
+ };
584
+ return {
585
+ config: normalized,
586
+ diagnostics: normalized.expression?.cron
587
+ ? createCronDiagnostics(normalized.expression.cron, normalized.expression.dialect)
588
+ : [],
589
+ };
590
+ }
591
+ function createCronDiagnostics(cron, dialect) {
592
+ const diagnostics = [];
593
+ const fieldCount = cron.trim().split(/\s+/).filter(Boolean).length;
594
+ if (dialect === 'aws-eventbridge' && fieldCount !== 6) {
595
+ diagnostics.push({
596
+ severity: 'error',
597
+ code: 'cron.aws.fieldCount',
598
+ field: 'expression.cron',
599
+ message: 'AWS EventBridge schedules require six fields.',
600
+ suggestion: 'Use the AWS order: minutes hours day-of-month month day-of-week year.',
601
+ });
602
+ }
603
+ else if (dialect !== 'aws-eventbridge' && !isValidCronExpression(cron)) {
604
+ diagnostics.push({
605
+ severity: 'error',
606
+ code: 'cron.invalid',
607
+ field: 'expression.cron',
608
+ message: 'Invalid CRON expression for the selected dialect.',
609
+ suggestion: 'Check field count, ranges, steps and unsupported special characters.',
610
+ });
611
+ }
612
+ if (dialect === 'unix' && fieldCount !== 5) {
613
+ diagnostics.push({
614
+ severity: 'error',
615
+ code: 'cron.unix.fieldCount',
616
+ field: 'expression.cron',
617
+ message: 'Unix cron schedules require five fields.',
618
+ suggestion: 'Remove seconds or year fields, or select a dialect that supports them.',
619
+ });
620
+ }
621
+ if (dialect === 'quartz' && fieldCount < 6) {
622
+ diagnostics.push({
623
+ severity: 'warning',
624
+ code: 'cron.quartz.fieldCount',
625
+ field: 'expression.cron',
626
+ message: 'Quartz cron commonly uses seconds as the first field.',
627
+ suggestion: 'Add a seconds field or select the Unix dialect.',
628
+ });
629
+ }
630
+ return diagnostics;
631
+ }
632
+
633
+ function compileScheduleExpression(config) {
634
+ const diagnostics = [];
635
+ const dialect = config.expression?.dialect ?? 'unix';
636
+ if (config.kind === 'customCron') {
637
+ if (!config.expression?.cron) {
638
+ diagnostics.push(createDiagnostic('error', 'schedule.cron.required', 'expression.cron', 'A CRON expression is required for custom schedules.'));
639
+ return { expression: null, diagnostics };
640
+ }
641
+ const seconds = config.expression.seconds ?? config.expression.cron.trim().split(/\s+/).length === 6;
642
+ return {
643
+ expression: {
644
+ cron: config.expression.cron.trim(),
645
+ dialect,
646
+ seconds,
647
+ },
648
+ diagnostics,
649
+ };
650
+ }
651
+ if (config.kind === 'once') {
652
+ diagnostics.push(createDiagnostic('warning', 'schedule.once.noCron', 'kind', 'One-time schedules do not have a portable CRON representation.'));
653
+ return { expression: null, diagnostics };
654
+ }
655
+ if (config.kind === 'interval') {
656
+ return compileIntervalSchedule(config, dialect);
657
+ }
658
+ if (config.kind === 'daily') {
659
+ return compileDailySchedule(config, dialect);
660
+ }
661
+ if (config.kind === 'weekly') {
662
+ return compileWeeklySchedule(config, dialect);
663
+ }
664
+ if (config.kind === 'monthly') {
665
+ return compileMonthlySchedule(config, dialect);
666
+ }
667
+ diagnostics.push(createDiagnostic('error', 'schedule.kind.unsupported', 'kind', 'Unsupported schedule kind.'));
668
+ return { expression: null, diagnostics };
669
+ }
670
+ function validateScheduleAuthoringConfig(config) {
671
+ const compiled = compileScheduleExpression(config);
672
+ const diagnostics = [...compiled.diagnostics];
673
+ if (compiled.expression && compiled.expression.dialect !== 'aws-eventbridge' && !isValidCronExpression(compiled.expression.cron)) {
674
+ diagnostics.push(createDiagnostic('error', 'schedule.cron.invalid', 'expression.cron', 'The compiled CRON expression is invalid.'));
675
+ }
676
+ if (!config.timezone) {
677
+ diagnostics.push(createDiagnostic('warning', 'schedule.timezone.defaulted', 'timezone', 'Timezone is missing and will default to UTC.'));
678
+ }
679
+ if (config.window?.startAt && config.window?.endAt && Date.parse(config.window.startAt) > Date.parse(config.window.endAt)) {
680
+ diagnostics.push(createDiagnostic('error', 'schedule.window.invalid', 'window', 'The schedule start date must be before the end date.'));
681
+ }
682
+ return diagnostics;
683
+ }
684
+ function createSchedulePreview(value, preview = {}) {
685
+ const normalized = normalizeScheduleValue(value, {
686
+ timezone: preview.timezone,
687
+ locale: preview.locale,
688
+ dialect: preview.dialect,
689
+ previewOccurrences: preview.occurrences,
690
+ });
691
+ const config = normalized.config;
692
+ if (!config) {
693
+ return {
694
+ config: null,
695
+ expression: null,
696
+ humanized: '',
697
+ occurrences: [],
698
+ diagnostics: normalized.diagnostics,
699
+ };
700
+ }
701
+ const compiled = compileScheduleExpression(config);
702
+ const diagnostics = mergeDiagnostics([
703
+ ...normalized.diagnostics,
704
+ ...validateScheduleAuthoringConfig(config),
705
+ ]);
706
+ if (!compiled.expression || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
707
+ return {
708
+ config,
709
+ expression: compiled.expression,
710
+ humanized: '',
711
+ occurrences: [],
712
+ diagnostics,
713
+ };
714
+ }
715
+ const from = preview.from ?? config.preview?.from ?? config.window?.startAt;
716
+ const count = preview.occurrences ?? config.preview?.occurrences ?? 5;
717
+ const currentDate = from ? new Date(from) : new Date();
718
+ const timezone = config.timezone || 'UTC';
719
+ const occurrences = getNextCronOccurrences(compiled.expression.cron, count, {
720
+ currentDate,
721
+ timeZone: timezone,
722
+ }).map((date) => ({
723
+ instant: date.toISOString(),
724
+ localDateTime: formatLocalDateTime(date, timezone, config.locale ?? 'en-US'),
725
+ timezone,
726
+ }));
727
+ return {
728
+ config,
729
+ expression: compiled.expression,
730
+ humanized: humanizeCronExpression(compiled.expression.cron, config.locale ?? 'en-US'),
731
+ occurrences,
732
+ diagnostics,
733
+ };
734
+ }
735
+ function compileIntervalSchedule(config, dialect) {
736
+ const interval = config.recurrence?.interval;
737
+ const diagnostics = [];
738
+ if (!interval || interval.every < 1) {
739
+ diagnostics.push(createDiagnostic('error', 'schedule.interval.invalid', 'recurrence.interval', 'Interval schedules require a positive interval.'));
740
+ return { expression: null, diagnostics };
741
+ }
742
+ if (interval.unit === 'minutes') {
743
+ if (interval.every > 59) {
744
+ diagnostics.push(createDiagnostic('error', 'schedule.interval.minutes.range', 'recurrence.interval.every', 'Minute intervals must be between 1 and 59.'));
745
+ return { expression: null, diagnostics };
746
+ }
747
+ return makeExpression(`*/${interval.every} * * * *`, dialect, false, diagnostics);
748
+ }
749
+ if (interval.unit === 'hours') {
750
+ if (interval.every > 23) {
751
+ diagnostics.push(createDiagnostic('error', 'schedule.interval.hours.range', 'recurrence.interval.every', 'Hourly intervals must be between 1 and 23.'));
752
+ return { expression: null, diagnostics };
753
+ }
754
+ return makeExpression(`0 */${interval.every} * * *`, dialect, false, diagnostics);
755
+ }
756
+ diagnostics.push(createDiagnostic('warning', 'schedule.interval.notPortable', 'recurrence.interval.unit', 'Day and week intervals are not portable CRON schedules without an anchor date.'));
757
+ return { expression: null, diagnostics };
758
+ }
759
+ function compileDailySchedule(config, dialect) {
760
+ const daily = config.recurrence?.daily;
761
+ const diagnostics = validateTimes(daily?.times, 'recurrence.daily.times');
762
+ if (!daily || diagnostics.length > 0) {
763
+ return { expression: null, diagnostics };
764
+ }
765
+ const time = parseTime(daily.times[0]);
766
+ const dayOfWeek = daily.onlyWeekdays ? '1-5' : '*';
767
+ return makeExpression(`${time.minute} ${time.hour} * * ${dayOfWeek}`, dialect, false, diagnostics);
768
+ }
769
+ function compileWeeklySchedule(config, dialect) {
770
+ const weekly = config.recurrence?.weekly;
771
+ const diagnostics = validateTimes(weekly?.times, 'recurrence.weekly.times');
772
+ if (!weekly || diagnostics.length > 0) {
773
+ return { expression: null, diagnostics };
774
+ }
775
+ if (!weekly.daysOfWeek?.length) {
776
+ diagnostics.push(createDiagnostic('error', 'schedule.weekly.days.required', 'recurrence.weekly.daysOfWeek', 'Weekly schedules require at least one weekday.'));
777
+ return { expression: null, diagnostics };
778
+ }
779
+ const time = parseTime(weekly.times[0]);
780
+ const days = normalizeWeekdays(weekly.daysOfWeek).join(',');
781
+ return makeExpression(`${time.minute} ${time.hour} * * ${days}`, dialect, false, diagnostics);
782
+ }
783
+ function compileMonthlySchedule(config, dialect) {
784
+ const monthly = config.recurrence?.monthly;
785
+ const diagnostics = validateTimes(monthly?.times, 'recurrence.monthly.times');
786
+ if (!monthly || diagnostics.length > 0) {
787
+ return { expression: null, diagnostics };
788
+ }
789
+ const time = parseTime(monthly.times[0]);
790
+ if (monthly.mode === 'dayOfMonth') {
791
+ if (!monthly.dayOfMonth || monthly.dayOfMonth < 1 || monthly.dayOfMonth > 31) {
792
+ diagnostics.push(createDiagnostic('error', 'schedule.monthly.day.range', 'recurrence.monthly.dayOfMonth', 'Monthly day must be between 1 and 31.'));
793
+ return { expression: null, diagnostics };
794
+ }
795
+ return makeExpression(`${time.minute} ${time.hour} ${monthly.dayOfMonth} * *`, dialect, false, diagnostics);
796
+ }
797
+ if (monthly.mode === 'nthWeekday') {
798
+ if (!monthly.nth || monthly.weekday == null) {
799
+ diagnostics.push(createDiagnostic('error', 'schedule.monthly.nth.required', 'recurrence.monthly', 'Nth weekday schedules require nth and weekday.'));
800
+ return { expression: null, diagnostics };
801
+ }
802
+ return makeExpression(`${time.minute} ${time.hour} ? * ${monthly.weekday}#${monthly.nth}`, dialect, false, diagnostics);
803
+ }
804
+ diagnostics.push(createDiagnostic('warning', 'schedule.monthly.mode.notPortable', 'recurrence.monthly.mode', 'This monthly mode needs a dialect-specific compiler before it can produce CRON.'));
805
+ return { expression: null, diagnostics };
806
+ }
807
+ function makeExpression(cron, dialect, seconds, diagnostics) {
808
+ if (dialect === 'quartz') {
809
+ return {
810
+ expression: {
811
+ cron: seconds ? cron : `0 ${cron}`,
812
+ dialect,
813
+ seconds: true,
814
+ },
815
+ diagnostics,
816
+ };
817
+ }
818
+ return {
819
+ expression: {
820
+ cron,
821
+ dialect,
822
+ seconds,
823
+ },
824
+ diagnostics,
825
+ };
826
+ }
827
+ function validateTimes(times, field) {
828
+ if (!times?.length) {
829
+ return [createDiagnostic('error', 'schedule.time.required', field, 'At least one time is required.')];
830
+ }
831
+ if (times.length > 1) {
832
+ return [createDiagnostic('warning', 'schedule.time.multiple.unsupported', field, 'Only the first time is compiled in this initial scheduler runtime.')];
833
+ }
834
+ const time = parseTime(times[0]);
835
+ if (time.hour < 0 || time.hour > 23 || time.minute < 0 || time.minute > 59) {
836
+ return [createDiagnostic('error', 'schedule.time.invalid', field, 'Time must use HH:mm with a 24-hour clock.')];
837
+ }
838
+ return [];
839
+ }
840
+ function parseTime(value) {
841
+ if (!/^\d{2}:\d{2}$/.test(value)) {
842
+ return { hour: Number.NaN, minute: Number.NaN };
843
+ }
844
+ const [hour, minute] = value.split(':').map((part) => Number(part));
845
+ return { hour, minute };
846
+ }
847
+ function normalizeWeekdays(days) {
848
+ return Array.from(new Set(days)).sort((a, b) => a - b);
849
+ }
850
+ function formatLocalDateTime(date, timezone, locale) {
851
+ return new Intl.DateTimeFormat(locale, {
852
+ timeZone: timezone,
853
+ dateStyle: 'full',
854
+ timeStyle: 'long',
855
+ }).format(date);
856
+ }
857
+ function mergeDiagnostics(diagnostics) {
858
+ const seen = new Set();
859
+ return diagnostics.filter((diagnostic) => {
860
+ const key = `${diagnostic.severity}:${diagnostic.code}:${diagnostic.field ?? ''}:${diagnostic.message}`;
861
+ if (seen.has(key)) {
862
+ return false;
863
+ }
864
+ seen.add(key);
865
+ return true;
866
+ });
867
+ }
868
+ function createDiagnostic(severity, code, field, message) {
869
+ return {
870
+ severity,
871
+ code,
872
+ field,
873
+ message,
874
+ };
875
+ }
876
+
432
877
  class PdxCronBuilderComponent {
433
878
  // Tipos auxiliares para Typed Forms
434
879
  fb = inject(NonNullableFormBuilder);
@@ -462,6 +907,9 @@ class PdxCronBuilderComponent {
462
907
  destroy$ = new Subject();
463
908
  humanized = '';
464
909
  preview = [];
910
+ structuredPreview = [];
911
+ scheduleConfig = null;
912
+ scheduleDiagnostics = [];
465
913
  error = null;
466
914
  // Opções expostas ao template para controle de fluxo moderno
467
915
  weekdayLabels = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
@@ -522,7 +970,7 @@ class PdxCronBuilderComponent {
522
970
  .subscribe((tz) => {
523
971
  this.metadata.timezone = tz || 'UTC';
524
972
  if (typeof this.value === 'string') {
525
- this.generatePreview(this.value);
973
+ this.refreshScheduleState(this.value);
526
974
  }
527
975
  });
528
976
  }
@@ -569,9 +1017,7 @@ class PdxCronBuilderComponent {
569
1017
  if (emitEvent) {
570
1018
  this.onChange(cron);
571
1019
  }
572
- this.validate(cron);
573
- this.humanize(cron);
574
- this.generatePreview(cron);
1020
+ this.refreshScheduleState(cron);
575
1021
  }
576
1022
  copyCron() {
577
1023
  if (typeof this.value === 'string') {
@@ -595,27 +1041,22 @@ class PdxCronBuilderComponent {
595
1041
  this.syncSimpleForm(cron);
596
1042
  }
597
1043
  }
598
- validate(cron) {
599
- this.error = isValidCronExpression(cron)
600
- ? null
601
- : this.metadata.validators?.invalidCronMessage || 'Invalid CRON expression';
602
- }
603
- humanize(cron) {
604
- if (this.error) {
605
- this.humanized = '';
606
- return;
607
- }
608
- this.humanized = humanizeCronExpression(cron, this.metadata.locale || 'en-US');
609
- }
610
- generatePreview(cron) {
611
- if (this.error || !this.metadata.previewOccurrences) {
612
- this.preview = [];
613
- return;
614
- }
615
- this.preview = getNextCronOccurrences(cron, this.metadata.previewOccurrences ?? 5, {
616
- currentDate: this.metadata.previewFrom ?? new Date(),
617
- timeZone: this.metadata.timezone,
1044
+ refreshScheduleState(cron) {
1045
+ const preview = createSchedulePreview(cron, {
1046
+ occurrences: this.metadata.previewOccurrences ?? 5,
1047
+ from: this.metadata.previewFrom?.toISOString(),
1048
+ timezone: this.metadata.timezone || this.timezoneControl.value || 'UTC',
1049
+ locale: this.metadata.locale || 'en-US',
618
1050
  });
1051
+ this.scheduleConfig = preview.config;
1052
+ this.scheduleDiagnostics = preview.diagnostics;
1053
+ this.structuredPreview = preview.occurrences;
1054
+ this.humanized = preview.humanized;
1055
+ this.preview = preview.occurrences.map((occurrence) => new Date(occurrence.instant));
1056
+ const error = preview.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
1057
+ this.error = error
1058
+ ? this.metadata.validators?.invalidCronMessage || error.message || 'Invalid CRON expression'
1059
+ : null;
619
1060
  }
620
1061
  parseCronString(cron) {
621
1062
  const parts = cron.trim().split(/\s+/);
@@ -806,6 +1247,12 @@ const PDX_CRON_BUILDER_DOC_META_LEGACY = {
806
1247
  */
807
1248
  const ENUMS = {
808
1249
  mode: ['simple', 'advanced', 'both'],
1250
+ scheduleKind: ['once', 'interval', 'daily', 'weekly', 'monthly', 'customCron'],
1251
+ cronDialect: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'],
1252
+ intervalUnit: ['minutes', 'hours', 'days', 'weeks'],
1253
+ monthlyMode: ['dayOfMonth', 'nthWeekday', 'lastDay', 'lastWeekday'],
1254
+ misfirePolicy: ['skip', 'run-late', 'run-once'],
1255
+ concurrencyPolicy: ['allow', 'forbid', 'replace'],
809
1256
  validationTrigger: ['change', 'blur'],
810
1257
  errorPosition: ['tooltip', 'inline'],
811
1258
  };
@@ -816,6 +1263,8 @@ const CRON_BUILDER_AI_CAPABILITIES = {
816
1263
  notes: [
817
1264
  'presets[] should be merged by cron or label to avoid replacing all entries.',
818
1265
  'metadata.previewFrom expects an ISO date string when provided.',
1266
+ 'schedule is the canonical scheduler authoring state; value remains the current CRON string compatibility surface.',
1267
+ 'Prefer schedule.kind and schedule.recurrence for business intent; use schedule.expression.cron only for custom CRON edits.',
819
1268
  ],
820
1269
  capabilities: [
821
1270
  { path: 'label', category: 'meta', valueKind: 'string', description: 'Field label (if exposed by host).' },
@@ -848,6 +1297,290 @@ const CRON_BUILDER_AI_CAPABILITIES = {
848
1297
  { path: 'metadata.errorPosition', category: 'ui', valueKind: 'enum', allowedValues: ENUMS.errorPosition, description: 'Error display position.' },
849
1298
  { path: 'metadata.showInlineErrors', category: 'ui', valueKind: 'boolean', description: 'Show inline errors.' },
850
1299
  { path: 'metadata.hint', category: 'ui', valueKind: 'string', description: 'Hint text.' },
1300
+ { path: 'schedule', category: 'schedule', valueKind: 'object', description: 'Canonical scheduler authoring config.' },
1301
+ { path: 'schedule.kind', category: 'schedule', valueKind: 'enum', allowedValues: ENUMS.scheduleKind, description: 'Business schedule intent.' },
1302
+ { path: 'schedule.timezone', category: 'i18n', valueKind: 'string', description: 'IANA timezone used by the schedule.' },
1303
+ { path: 'schedule.locale', category: 'i18n', valueKind: 'string', description: 'Locale used by humanized text and preview.' },
1304
+ { path: 'schedule.expression', category: 'dialects', valueKind: 'object', description: 'Technical CRON representation.' },
1305
+ { path: 'schedule.expression.cron', category: 'dialects', valueKind: 'string', description: 'CRON expression for custom or compiled schedules.' },
1306
+ { path: 'schedule.expression.dialect', category: 'dialects', valueKind: 'enum', allowedValues: ENUMS.cronDialect, description: 'CRON dialect family.' },
1307
+ { path: 'schedule.expression.seconds', category: 'dialects', valueKind: 'boolean', description: 'Whether the CRON representation includes seconds.' },
1308
+ { path: 'schedule.recurrence', category: 'recurrence', valueKind: 'object', description: 'Structured recurrence intent.' },
1309
+ { path: 'schedule.recurrence.once.runAt', category: 'recurrence', valueKind: 'string', description: 'ISO date-time for one-time schedules.' },
1310
+ { path: 'schedule.recurrence.interval.every', category: 'recurrence', valueKind: 'number', description: 'Interval amount.' },
1311
+ { path: 'schedule.recurrence.interval.unit', category: 'recurrence', valueKind: 'enum', allowedValues: ENUMS.intervalUnit, description: 'Interval unit.' },
1312
+ { path: 'schedule.recurrence.daily.times', category: 'recurrence', valueKind: 'array', description: 'Daily run times in HH:mm format.' },
1313
+ { path: 'schedule.recurrence.daily.onlyWeekdays', category: 'recurrence', valueKind: 'boolean', description: 'Limit daily schedule to Monday through Friday.' },
1314
+ { path: 'schedule.recurrence.weekly.daysOfWeek', category: 'recurrence', valueKind: 'array', description: 'Weekdays as numbers where 0 is Sunday.' },
1315
+ { path: 'schedule.recurrence.weekly.times', category: 'recurrence', valueKind: 'array', description: 'Weekly run times in HH:mm format.' },
1316
+ { path: 'schedule.recurrence.monthly.mode', category: 'recurrence', valueKind: 'enum', allowedValues: ENUMS.monthlyMode, description: 'Monthly recurrence strategy.' },
1317
+ { path: 'schedule.recurrence.monthly.dayOfMonth', category: 'recurrence', valueKind: 'number', description: 'Day of month for monthly schedules.' },
1318
+ { path: 'schedule.recurrence.monthly.nth', category: 'recurrence', valueKind: 'number', description: 'Nth weekday occurrence in the month.' },
1319
+ { path: 'schedule.recurrence.monthly.weekday', category: 'recurrence', valueKind: 'number', description: 'Weekday for nth weekday monthly schedules.' },
1320
+ { path: 'schedule.recurrence.monthly.times', category: 'recurrence', valueKind: 'array', description: 'Monthly run times in HH:mm format.' },
1321
+ { path: 'schedule.window.startAt', category: 'policy', valueKind: 'string', description: 'ISO start date-time for schedule validity.' },
1322
+ { path: 'schedule.window.endAt', category: 'policy', valueKind: 'string', description: 'ISO end date-time for schedule validity.' },
1323
+ { path: 'schedule.executionPolicy.enabled', category: 'policy', valueKind: 'boolean', description: 'Whether the schedule is active.' },
1324
+ { path: 'schedule.executionPolicy.misfirePolicy', category: 'policy', valueKind: 'enum', allowedValues: ENUMS.misfirePolicy, description: 'Policy for missed scheduled runs.' },
1325
+ { path: 'schedule.executionPolicy.concurrencyPolicy', category: 'policy', valueKind: 'enum', allowedValues: ENUMS.concurrencyPolicy, description: 'Policy for overlapping runs.' },
1326
+ { path: 'schedule.executionPolicy.flexibleWindowMinutes', category: 'policy', valueKind: 'number', description: 'Flexible delivery window in minutes.' },
1327
+ { path: 'schedule.executionPolicy.jitterMinutes', category: 'policy', valueKind: 'number', description: 'Randomized delay window in minutes.' },
1328
+ { path: 'schedule.preview.occurrences', category: 'preview', valueKind: 'number', description: 'Number of occurrences to preview.' },
1329
+ { path: 'schedule.preview.from', category: 'preview', valueKind: 'string', description: 'ISO date-time used as preview start.' },
1330
+ { path: 'schedule.governance.name', category: 'governance', valueKind: 'string', description: 'Schedule name.' },
1331
+ { path: 'schedule.governance.description', category: 'governance', valueKind: 'string', description: 'Schedule description.' },
1332
+ { path: 'schedule.governance.owner', category: 'governance', valueKind: 'string', description: 'Schedule owner.' },
1333
+ { path: 'schedule.governance.tags', category: 'governance', valueKind: 'array', description: 'Governance tags.' },
1334
+ { path: 'diagnostics', category: 'validation', valueKind: 'array', description: 'Read-only structured schedule diagnostics.' },
1335
+ { path: 'preview', category: 'preview', valueKind: 'array', description: 'Read-only structured preview occurrences.' },
1336
+ ],
1337
+ };
1338
+
1339
+ const cronDialectEnum = ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'];
1340
+ const scheduleKindEnum = ['once', 'interval', 'daily', 'weekly', 'monthly', 'customCron'];
1341
+ const PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST = {
1342
+ schemaVersion: '1.0.0',
1343
+ componentId: 'pdx-cron-builder',
1344
+ ownerPackage: '@praxisui/cron-builder',
1345
+ configSchemaId: 'CronBuilderMetadata',
1346
+ manifestVersion: '1.0.0',
1347
+ runtimeInputs: [
1348
+ { name: 'metadata', type: 'CronBuilderMetadata', description: 'Builder UI, timezone, locale, presets, validation and preview configuration.' },
1349
+ { name: 'value', type: 'string | ScheduleAuthoringConfig', description: 'Current CRON string compatibility value or canonical schedule authoring config.' },
1350
+ { name: 'disabled', type: 'boolean', description: 'ControlValueAccessor disabled state.' },
1351
+ ],
1352
+ editableTargets: [
1353
+ { kind: 'expression', resolver: 'schedule-expression', description: 'Canonical CRON expression under schedule.expression or legacy value.' },
1354
+ { kind: 'frequency', resolver: 'schedule-kind-and-recurrence', description: 'Business recurrence intent compiled into a canonical expression.' },
1355
+ { kind: 'timezone', resolver: 'schedule-timezone', description: 'IANA timezone used for preview and schedule metadata.' },
1356
+ { kind: 'preview', resolver: 'schedule-preview-config', description: 'Read-only preview generation request for next occurrences.' },
1357
+ { kind: 'validation', resolver: 'schedule-validation-request', description: 'Read-only validation diagnostics request for expression or schedule config.' },
1358
+ { kind: 'preset', resolver: 'cron-preset-by-label-or-expression', description: 'Preset entry in metadata.presets[] or top-level presets[].' },
1359
+ ],
1360
+ operations: [
1361
+ {
1362
+ operationId: 'cron.expression.set',
1363
+ title: 'Set CRON expression',
1364
+ scope: 'global',
1365
+ targetKind: 'expression',
1366
+ target: { kind: 'expression', resolver: 'schedule-expression', ambiguityPolicy: 'fail', required: false },
1367
+ inputSchema: {
1368
+ type: 'object',
1369
+ required: ['cron'],
1370
+ properties: {
1371
+ cron: { type: 'string' },
1372
+ dialect: { enum: cronDialectEnum },
1373
+ seconds: { type: 'boolean' },
1374
+ },
1375
+ },
1376
+ effects: [{ kind: 'merge-object', path: 'schedule.expression' }, { kind: 'set-value', path: 'schedule.kind' }],
1377
+ validators: ['cron-expression-valid', 'cron-dialect-compatible', 'editor-runtime-round-trip'],
1378
+ affectedPaths: ['schedule.kind', 'schedule.expression.cron', 'schedule.expression.dialect', 'schedule.expression.seconds', 'value'],
1379
+ submissionImpact: true,
1380
+ preconditions: ['config-initialized'],
1381
+ },
1382
+ {
1383
+ operationId: 'cron.frequency.set',
1384
+ title: 'Set schedule frequency',
1385
+ scope: 'global',
1386
+ targetKind: 'frequency',
1387
+ target: { kind: 'frequency', resolver: 'schedule-kind-and-recurrence', ambiguityPolicy: 'fail', required: false },
1388
+ inputSchema: {
1389
+ type: 'object',
1390
+ required: ['kind', 'recurrence'],
1391
+ properties: {
1392
+ kind: { enum: scheduleKindEnum },
1393
+ recurrence: { type: 'object' },
1394
+ dialect: { enum: cronDialectEnum },
1395
+ },
1396
+ },
1397
+ effects: [{
1398
+ kind: 'compile-domain-patch',
1399
+ handler: 'cron-frequency-to-expression',
1400
+ handlerContract: {
1401
+ reads: ['schedule.kind', 'schedule.recurrence', 'schedule.timezone', 'schedule.expression.dialect'],
1402
+ writes: ['schedule.kind', 'schedule.recurrence', 'schedule.expression', 'value'],
1403
+ identityKeys: ['schedule.kind'],
1404
+ inputSchema: {
1405
+ type: 'object',
1406
+ required: ['kind', 'recurrence'],
1407
+ properties: {
1408
+ kind: { enum: scheduleKindEnum },
1409
+ recurrence: { type: 'object' },
1410
+ dialect: { enum: cronDialectEnum },
1411
+ },
1412
+ },
1413
+ failureModes: ['unsupported-kind', 'non-portable-frequency', 'invalid-recurrence', 'cron-compile-failed'],
1414
+ description: 'Compiles structured recurrence intent through compileScheduleExpression and updates the canonical schedule expression only when diagnostics have no errors.',
1415
+ },
1416
+ }],
1417
+ validators: ['frequency-maps-to-canonical-expression', 'cron-expression-valid', 'diagnostics-before-patch'],
1418
+ affectedPaths: ['schedule.kind', 'schedule.recurrence', 'schedule.expression', 'value'],
1419
+ submissionImpact: true,
1420
+ preconditions: ['config-initialized'],
1421
+ },
1422
+ {
1423
+ operationId: 'cron.timezone.set',
1424
+ title: 'Set schedule timezone',
1425
+ scope: 'global',
1426
+ targetKind: 'timezone',
1427
+ target: { kind: 'timezone', resolver: 'schedule-timezone', ambiguityPolicy: 'fail', required: false },
1428
+ inputSchema: { type: 'object', required: ['timezone'], properties: { timezone: { type: 'string' } } },
1429
+ effects: [{ kind: 'set-value', path: 'schedule.timezone' }, { kind: 'set-value', path: 'metadata.timezone' }],
1430
+ validators: ['timezone-valid', 'preview-matches-expression', 'editor-runtime-round-trip'],
1431
+ affectedPaths: ['schedule.timezone', 'metadata.timezone', 'preview'],
1432
+ submissionImpact: true,
1433
+ preconditions: ['config-initialized'],
1434
+ },
1435
+ {
1436
+ operationId: 'cron.preset.apply',
1437
+ title: 'Apply CRON preset',
1438
+ scope: 'global',
1439
+ targetKind: 'preset',
1440
+ target: { kind: 'preset', resolver: 'cron-preset-by-label-or-expression', ambiguityPolicy: 'fail', required: true },
1441
+ inputSchema: {
1442
+ type: 'object',
1443
+ required: ['labelOrCron'],
1444
+ properties: {
1445
+ labelOrCron: { type: 'string' },
1446
+ timezone: { type: 'string' },
1447
+ },
1448
+ },
1449
+ effects: [{
1450
+ kind: 'compile-domain-patch',
1451
+ handler: 'cron-preset-apply',
1452
+ handlerContract: {
1453
+ reads: ['metadata.presets[]', 'presets[]', 'schedule.timezone', 'metadata.timezone'],
1454
+ writes: ['schedule.kind', 'schedule.expression', 'schedule.timezone', 'value'],
1455
+ identityKeys: ['metadata.presets[].label', 'metadata.presets[].cron', 'presets[].label', 'presets[].cron'],
1456
+ inputSchema: {
1457
+ type: 'object',
1458
+ required: ['labelOrCron'],
1459
+ properties: {
1460
+ labelOrCron: { type: 'string' },
1461
+ timezone: { type: 'string' },
1462
+ },
1463
+ },
1464
+ failureModes: ['preset-not-found', 'preset-cron-invalid', 'timezone-invalid'],
1465
+ description: 'Resolves a preset by label or cron, validates it through normalizeScheduleValue/createSchedulePreview, then applies the canonical expression.',
1466
+ },
1467
+ }],
1468
+ validators: ['preset-exists', 'preset-maps-to-canonical-expression', 'cron-expression-valid'],
1469
+ affectedPaths: ['schedule.kind', 'schedule.expression', 'schedule.timezone', 'value'],
1470
+ submissionImpact: true,
1471
+ preconditions: ['config-initialized', 'target-preset-exists'],
1472
+ },
1473
+ {
1474
+ operationId: 'cron.validate',
1475
+ title: 'Validate schedule',
1476
+ scope: 'global',
1477
+ targetKind: 'validation',
1478
+ target: { kind: 'validation', resolver: 'schedule-validation-request', ambiguityPolicy: 'fail', required: false },
1479
+ inputSchema: {
1480
+ type: 'object',
1481
+ properties: {
1482
+ cron: { type: 'string' },
1483
+ schedule: { type: 'object' },
1484
+ timezone: { type: 'string' },
1485
+ dialect: { enum: cronDialectEnum },
1486
+ },
1487
+ },
1488
+ effects: [{
1489
+ kind: 'compile-domain-patch',
1490
+ handler: 'cron-validation-diagnostics',
1491
+ handlerContract: {
1492
+ reads: ['value', 'schedule', 'metadata.timezone'],
1493
+ writes: ['diagnostics'],
1494
+ identityKeys: ['schedule.expression.cron', 'schedule.timezone'],
1495
+ inputSchema: {
1496
+ type: 'object',
1497
+ properties: {
1498
+ cron: { type: 'string' },
1499
+ schedule: { type: 'object' },
1500
+ timezone: { type: 'string' },
1501
+ dialect: { enum: cronDialectEnum },
1502
+ },
1503
+ },
1504
+ failureModes: ['invalid-expression', 'invalid-timezone', 'invalid-recurrence'],
1505
+ description: 'Runs normalizeScheduleValue and validateScheduleAuthoringConfig and returns diagnostics without mutating schedule/value paths.',
1506
+ },
1507
+ }],
1508
+ validators: ['invalid-schedules-return-diagnostics', 'cron-expression-valid', 'timezone-valid'],
1509
+ affectedPaths: ['diagnostics'],
1510
+ submissionImpact: false,
1511
+ preconditions: ['config-initialized'],
1512
+ },
1513
+ {
1514
+ operationId: 'cron.preview.generate',
1515
+ title: 'Generate schedule preview',
1516
+ scope: 'global',
1517
+ targetKind: 'preview',
1518
+ target: { kind: 'preview', resolver: 'schedule-preview-config', ambiguityPolicy: 'fail', required: false },
1519
+ inputSchema: {
1520
+ type: 'object',
1521
+ properties: {
1522
+ occurrences: { type: 'number' },
1523
+ from: { type: 'string' },
1524
+ timezone: { type: 'string' },
1525
+ locale: { type: 'string' },
1526
+ },
1527
+ },
1528
+ effects: [{
1529
+ kind: 'compile-domain-patch',
1530
+ handler: 'cron-preview-generate',
1531
+ handlerContract: {
1532
+ reads: ['value', 'schedule', 'metadata.previewOccurrences', 'metadata.previewFrom', 'metadata.timezone', 'metadata.locale'],
1533
+ writes: ['preview', 'diagnostics'],
1534
+ identityKeys: ['schedule.expression.cron', 'schedule.timezone', 'metadata.previewFrom'],
1535
+ inputSchema: {
1536
+ type: 'object',
1537
+ properties: {
1538
+ occurrences: { type: 'number' },
1539
+ from: { type: 'string' },
1540
+ timezone: { type: 'string' },
1541
+ locale: { type: 'string' },
1542
+ },
1543
+ },
1544
+ failureModes: ['invalid-expression', 'invalid-timezone', 'preview-empty', 'diagnostics-contain-errors'],
1545
+ description: 'Runs createSchedulePreview and returns read-only preview occurrences plus diagnostics; it does not patch the schedule when validation fails.',
1546
+ },
1547
+ }],
1548
+ validators: ['preview-matches-expression', 'timezone-valid', 'invalid-schedules-return-diagnostics'],
1549
+ affectedPaths: ['preview', 'diagnostics'],
1550
+ submissionImpact: false,
1551
+ preconditions: ['config-initialized'],
1552
+ },
1553
+ ],
1554
+ validators: [
1555
+ { validatorId: 'cron-expression-valid', level: 'error', code: 'PCRON001', description: 'CRON expressions must pass the canonical cron-runtime parser for the selected dialect before schedule/value paths are patched.' },
1556
+ { validatorId: 'cron-dialect-compatible', level: 'error', code: 'PCRON002', description: 'Field count, seconds support and dialect-specific rules must match CRON_DIALECTS.' },
1557
+ { validatorId: 'timezone-valid', level: 'error', code: 'PCRON003', description: 'Timezone must be a valid IANA timezone accepted by Intl.DateTimeFormat and schedule preview.' },
1558
+ { validatorId: 'frequency-maps-to-canonical-expression', level: 'error', code: 'PCRON004', description: 'Structured frequency intent must compile through compileScheduleExpression into the canonical expression when portable.' },
1559
+ { validatorId: 'preset-exists', level: 'error', code: 'PCRON005', description: 'Preset operations must resolve an existing preset by label or cron expression.' },
1560
+ { validatorId: 'preset-maps-to-canonical-expression', level: 'error', code: 'PCRON006', description: 'Preset cron values must normalize into ScheduleAuthoringConfig and a valid canonical expression.' },
1561
+ { validatorId: 'preview-matches-expression', level: 'error', code: 'PCRON007', description: 'Preview occurrences must be generated from the same expression, timezone and from-date requested by authoring.' },
1562
+ { validatorId: 'invalid-schedules-return-diagnostics', level: 'error', code: 'PCRON008', description: 'Validation and preview requests for invalid schedules must return diagnostics and must not patch schedule/value fields.' },
1563
+ { validatorId: 'diagnostics-before-patch', level: 'error', code: 'PCRON009', description: 'Operations that compile recurrence or presets must evaluate diagnostics before mutating canonical schedule/value paths.' },
1564
+ { validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PCRON010', description: 'ControlValueAccessor, runtime preview and AI adapter snapshots must preserve expression and timezone.' },
1565
+ ],
1566
+ roundTripRequirements: [
1567
+ 'Expression and timezone edits must round-trip through ControlValueAccessor, normalizeScheduleValue and createSchedulePreview without losing the selected timezone.',
1568
+ 'Structured frequency authoring must use ScheduleAuthoringConfig and compileScheduleExpression as the canonical source of recurrence semantics.',
1569
+ 'Validation and preview operations are read-only: invalid schedules return diagnostics and must not mutate schedule/value paths.',
1570
+ 'Preset operations must resolve stable label/cron identities and validate the resulting expression before applying it.',
1571
+ 'Preview generation must use the same expression, timezone, locale, occurrences and from-date that are visible in the schedule config or metadata input.',
1572
+ ],
1573
+ examples: [
1574
+ { id: 'weekday-0800', request: 'Run every weekday at 08:00.', operationId: 'cron.frequency.set', params: { kind: 'daily', recurrence: { daily: { times: ['08:00'], onlyWeekdays: true } }, dialect: 'unix' }, isPositive: true },
1575
+ { id: 'every-15-minutes', request: 'Run every 15 minutes.', operationId: 'cron.frequency.set', params: { kind: 'interval', recurrence: { interval: { every: 15, unit: 'minutes' } }, dialect: 'unix' }, isPositive: true },
1576
+ { id: 'monthly-first-day', request: 'Run monthly on the first day at 09:00.', operationId: 'cron.frequency.set', params: { kind: 'monthly', recurrence: { monthly: { mode: 'dayOfMonth', dayOfMonth: 1, times: ['09:00'] } }, dialect: 'unix' }, isPositive: true },
1577
+ { id: 'set-explicit-expression', request: 'Use cron expression 0 8 * * 1-5.', operationId: 'cron.expression.set', params: { cron: '0 8 * * 1-5', dialect: 'unix', seconds: false }, isPositive: true },
1578
+ { id: 'change-timezone-preview', request: 'Change timezone to America/Sao_Paulo and preview the next runs.', operationId: 'cron.timezone.set', params: { timezone: 'America/Sao_Paulo' }, isPositive: true },
1579
+ { id: 'preview-next-five', request: 'Preview the next five runs from 2026-04-20T00:00:00Z.', operationId: 'cron.preview.generate', params: { occurrences: 5, from: '2026-04-20T00:00:00.000Z', timezone: 'UTC' }, isPositive: true },
1580
+ { id: 'apply-business-hours-preset', request: 'Apply the business hours preset.', operationId: 'cron.preset.apply', target: 'business hours', params: { labelOrCron: 'business hours' }, isPositive: true },
1581
+ { id: 'reject-impossible-expression', request: 'Use cron expression 99 99 * * *.', operationId: 'cron.expression.set', params: { cron: '99 99 * * *', dialect: 'unix' }, isPositive: false },
1582
+ { id: 'diagnose-invalid-expression', request: 'Validate 99 99 * * * and explain why it is invalid.', operationId: 'cron.validate', params: { cron: '99 99 * * *', dialect: 'unix' }, isPositive: true },
1583
+ { id: 'reject-invalid-timezone', request: 'Change timezone to Mars/Olympus.', operationId: 'cron.timezone.set', params: { timezone: 'Mars/Olympus' }, isPositive: false },
851
1584
  ],
852
1585
  };
853
1586
 
@@ -859,4 +1592,4 @@ const CRON_BUILDER_AI_CAPABILITIES = {
859
1592
  * Generated bundle index. Do not edit.
860
1593
  */
861
1594
 
862
- export { CRON_BUILDER_AI_CAPABILITIES, PDX_CRON_BUILDER_DOC_META_LEGACY, PdxCronBuilderComponent };
1595
+ export { CRON_BUILDER_AI_CAPABILITIES, CRON_DIALECTS, PDX_CRON_BUILDER_DOC_META_LEGACY, PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST, PdxCronBuilderComponent, SCHEDULE_AUTHORING_CONFIG_VERSION, compileScheduleExpression, createSchedulePreview, getCronDialectDefinition, inferCronDialect, normalizeScheduleValue, validateScheduleAuthoringConfig };
package/index.d.ts CHANGED
@@ -2,7 +2,150 @@ import * as i0 from '@angular/core';
2
2
  import { OnInit, OnDestroy } from '@angular/core';
3
3
  import { ControlValueAccessor, FormGroup, FormControl } from '@angular/forms';
4
4
  import { MatTabChangeEvent } from '@angular/material/tabs';
5
- import { AiCapabilityCategory, AiValueKind, AiCapability, AiCapabilityCatalog } from '@praxisui/core';
5
+ import { AiCapabilityCategory, AiValueKind, AiCapability, AiCapabilityCatalog, ComponentAuthoringManifest } from '@praxisui/core';
6
+
7
+ declare const SCHEDULE_AUTHORING_CONFIG_VERSION: "v1";
8
+ type ScheduleAuthoringConfigVersion = typeof SCHEDULE_AUTHORING_CONFIG_VERSION;
9
+ type CronDialect = 'unix' | 'quartz' | 'aws-eventbridge' | 'kubernetes' | 'github-actions' | 'gcp-scheduler';
10
+ type ScheduleKind = 'once' | 'interval' | 'daily' | 'weekly' | 'monthly' | 'customCron';
11
+ type ScheduleTimeUnit = 'minutes' | 'hours' | 'days' | 'weeks';
12
+ type ScheduleWeekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
13
+ type ScheduleSeverity = 'error' | 'warning' | 'info';
14
+ interface CronDialectDefinition {
15
+ dialect: CronDialect;
16
+ label: string;
17
+ fieldCount: {
18
+ min: number;
19
+ max: number;
20
+ };
21
+ supportsSeconds: boolean;
22
+ supportsYear: boolean;
23
+ supportsNamedMonths: boolean;
24
+ supportsNamedWeekdays: boolean;
25
+ supportsQuestionMark: boolean;
26
+ supportsLast: boolean;
27
+ supportsNearestWeekday: boolean;
28
+ supportsNthWeekday: boolean;
29
+ timezoneMode: 'external-field' | 'host-local' | 'fixed-utc' | 'expression-prefix-unsupported';
30
+ dayOfMonthDayOfWeekRule: 'either-or-question-mark' | 'unix-or' | 'host-specific';
31
+ examples: readonly string[];
32
+ }
33
+ interface ScheduleDiagnostic {
34
+ severity: ScheduleSeverity;
35
+ code: string;
36
+ field?: string;
37
+ message: string;
38
+ suggestion?: string;
39
+ }
40
+ interface ScheduleCronExpression {
41
+ cron: string;
42
+ dialect: CronDialect;
43
+ seconds?: boolean;
44
+ }
45
+ interface OnceSchedule {
46
+ runAt: string;
47
+ }
48
+ interface IntervalSchedule {
49
+ every: number;
50
+ unit: ScheduleTimeUnit;
51
+ }
52
+ interface DailySchedule {
53
+ times: string[];
54
+ onlyWeekdays?: boolean;
55
+ }
56
+ interface WeeklySchedule {
57
+ daysOfWeek: ScheduleWeekday[];
58
+ times: string[];
59
+ }
60
+ interface MonthlySchedule {
61
+ mode: 'dayOfMonth' | 'nthWeekday' | 'lastDay' | 'lastWeekday';
62
+ dayOfMonth?: number;
63
+ nth?: 1 | 2 | 3 | 4 | 5;
64
+ weekday?: ScheduleWeekday;
65
+ times: string[];
66
+ }
67
+ interface ScheduleRecurrence {
68
+ once?: OnceSchedule;
69
+ interval?: IntervalSchedule;
70
+ daily?: DailySchedule;
71
+ weekly?: WeeklySchedule;
72
+ monthly?: MonthlySchedule;
73
+ }
74
+ interface ScheduleWindow {
75
+ startAt?: string;
76
+ endAt?: string;
77
+ }
78
+ interface ScheduleExecutionPolicy {
79
+ enabled?: boolean;
80
+ misfirePolicy?: 'skip' | 'run-late' | 'run-once';
81
+ concurrencyPolicy?: 'allow' | 'forbid' | 'replace';
82
+ flexibleWindowMinutes?: number;
83
+ jitterMinutes?: number;
84
+ }
85
+ interface SchedulePreviewConfig {
86
+ occurrences?: number;
87
+ from?: string;
88
+ }
89
+ interface ScheduleGovernance {
90
+ name?: string;
91
+ description?: string;
92
+ owner?: string;
93
+ tags?: string[];
94
+ }
95
+ interface ScheduleAuthoringConfig {
96
+ version: ScheduleAuthoringConfigVersion;
97
+ kind: ScheduleKind;
98
+ timezone: string;
99
+ locale?: string;
100
+ expression?: ScheduleCronExpression;
101
+ recurrence?: ScheduleRecurrence;
102
+ window?: ScheduleWindow;
103
+ executionPolicy?: ScheduleExecutionPolicy;
104
+ preview?: SchedulePreviewConfig;
105
+ governance?: ScheduleGovernance;
106
+ }
107
+ declare const CRON_DIALECTS: Record<CronDialect, CronDialectDefinition>;
108
+ declare function getCronDialectDefinition(dialect: CronDialect): CronDialectDefinition;
109
+
110
+ interface ScheduleNormalizeOptions {
111
+ timezone?: string;
112
+ locale?: string;
113
+ dialect?: CronDialect;
114
+ previewOccurrences?: number;
115
+ }
116
+ interface ScheduleNormalizeResult {
117
+ config: ScheduleAuthoringConfig | null;
118
+ diagnostics: ScheduleDiagnostic[];
119
+ }
120
+ type ScheduleInput = string | ScheduleAuthoringConfig | null | undefined;
121
+ declare function normalizeScheduleValue(value: ScheduleInput, options?: ScheduleNormalizeOptions): ScheduleNormalizeResult;
122
+ declare function inferCronDialect(expression: string): CronDialect;
123
+
124
+ interface CompiledScheduleExpression {
125
+ cron: string;
126
+ dialect: CronDialect;
127
+ seconds: boolean;
128
+ }
129
+ interface SchedulePreviewOccurrence {
130
+ instant: string;
131
+ localDateTime: string;
132
+ timezone: string;
133
+ }
134
+ interface SchedulePreviewResult {
135
+ config: ScheduleAuthoringConfig | null;
136
+ expression: CompiledScheduleExpression | null;
137
+ humanized: string;
138
+ occurrences: SchedulePreviewOccurrence[];
139
+ diagnostics: ScheduleDiagnostic[];
140
+ }
141
+ interface CreateSchedulePreviewOptions extends SchedulePreviewConfig, Pick<ScheduleNormalizeOptions, 'timezone' | 'locale' | 'dialect'> {
142
+ }
143
+ declare function compileScheduleExpression(config: ScheduleAuthoringConfig): {
144
+ expression: CompiledScheduleExpression | null;
145
+ diagnostics: ScheduleDiagnostic[];
146
+ };
147
+ declare function validateScheduleAuthoringConfig(config: ScheduleAuthoringConfig): ScheduleDiagnostic[];
148
+ declare function createSchedulePreview(value: string | ScheduleAuthoringConfig | null | undefined, preview?: CreateSchedulePreviewOptions): SchedulePreviewResult;
6
149
 
7
150
  interface Recurrence {
8
151
  }
@@ -70,6 +213,9 @@ declare class PdxCronBuilderComponent implements ControlValueAccessor, OnInit, O
70
213
  private readonly destroy$;
71
214
  humanized: string;
72
215
  preview: Date[];
216
+ structuredPreview: SchedulePreviewOccurrence[];
217
+ scheduleConfig: ScheduleAuthoringConfig | null;
218
+ scheduleDiagnostics: ScheduleDiagnostic[];
73
219
  error: string | null;
74
220
  readonly weekdayLabels: readonly ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
75
221
  readonly weeklyDayOptions: readonly [0, 1, 2, 3, 4, 5, 6];
@@ -91,9 +237,7 @@ declare class PdxCronBuilderComponent implements ControlValueAccessor, OnInit, O
91
237
  copyCron(): void;
92
238
  copyHumanized(): void;
93
239
  importCron(): void;
94
- private validate;
95
- private humanize;
96
- private generatePreview;
240
+ private refreshScheduleState;
97
241
  private parseCronString;
98
242
  private syncSimpleForm;
99
243
  private buildCronFromSimple;
@@ -116,9 +260,14 @@ declare const PDX_CRON_BUILDER_DOC_META_LEGACY: any;
116
260
  declare module '@praxisui/core' {
117
261
  interface AiCapabilityCategoryMap {
118
262
  meta: true;
263
+ schedule: true;
264
+ recurrence: true;
265
+ dialects: true;
119
266
  fields: true;
120
267
  presets: true;
268
+ policy: true;
121
269
  preview: true;
270
+ governance: true;
122
271
  i18n: true;
123
272
  ui: true;
124
273
  }
@@ -133,5 +282,7 @@ interface CapabilityCatalog extends AiCapabilityCatalog {
133
282
  }
134
283
  declare const CRON_BUILDER_AI_CAPABILITIES: CapabilityCatalog;
135
284
 
136
- export { CRON_BUILDER_AI_CAPABILITIES, PDX_CRON_BUILDER_DOC_META_LEGACY, PdxCronBuilderComponent };
137
- export type { AdvancedCronFormValue, Capability, CapabilityCatalog, CapabilityCategory, CronBuilderMetadata, CronPresetType, Recurrence, SimpleCronFormValue, ValueKind };
285
+ declare const PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST: ComponentAuthoringManifest;
286
+
287
+ export { CRON_BUILDER_AI_CAPABILITIES, CRON_DIALECTS, PDX_CRON_BUILDER_DOC_META_LEGACY, PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST, PdxCronBuilderComponent, SCHEDULE_AUTHORING_CONFIG_VERSION, compileScheduleExpression, createSchedulePreview, getCronDialectDefinition, inferCronDialect, normalizeScheduleValue, validateScheduleAuthoringConfig };
288
+ export type { AdvancedCronFormValue, Capability, CapabilityCatalog, CapabilityCategory, CompiledScheduleExpression, CreateSchedulePreviewOptions, CronBuilderMetadata, CronDialect, CronDialectDefinition, CronPresetType, DailySchedule, IntervalSchedule, MonthlySchedule, OnceSchedule, Recurrence, ScheduleAuthoringConfig, ScheduleAuthoringConfigVersion, ScheduleCronExpression, ScheduleDiagnostic, ScheduleExecutionPolicy, ScheduleGovernance, ScheduleInput, ScheduleKind, ScheduleNormalizeOptions, ScheduleNormalizeResult, SchedulePreviewConfig, SchedulePreviewOccurrence, SchedulePreviewResult, ScheduleRecurrence, ScheduleSeverity, ScheduleTimeUnit, ScheduleWeekday, ScheduleWindow, SimpleCronFormValue, ValueKind, WeeklySchedule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/cron-builder",
3
- "version": "8.0.0-beta.0",
3
+ "version": "8.0.0-beta.11",
4
4
  "description": "Cron expression builder utilities and components for Praxis UI.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^20.1.0",
@@ -8,7 +8,7 @@
8
8
  "@angular/forms": "^20.1.0",
9
9
  "@angular/cdk": "^20.1.0",
10
10
  "@angular/material": "^20.1.0",
11
- "@praxisui/core": "^8.0.0-beta.0"
11
+ "@praxisui/core": "^8.0.0-beta.11"
12
12
  },
13
13
  "dependencies": {
14
14
  "tslib": "^2.3.0",