@@ -223,13 +223,23 @@ public static function fact(string $label, string $value): array
223223 /**
224224 * Appends a compact strip of label and value pairs.
225225 *
226- * @param FactEntry ...$facts Entries produced by {@see self::fact()}.
226+ * @param array<array-key, mixed> ...$facts Entries produced by {@see self::fact()}.
227+ *
228+ * @throws InvalidArgumentException if an argument was not built by {@see self::fact()}.
227229 *
228230 * @return self New view with the fact strip appended.
229231 */
230232 public function facts (array ...$ facts ): self
231233 {
232- return $ this ->append (['kind ' => 'facts ' , 'facts ' => array_values ($ facts )]);
234+ $ entries = [];
235+
236+ foreach ($ facts as $ fact ) {
237+ self ::assertFactEntry ($ fact );
238+
239+ $ entries [] = $ fact ;
240+ }
241+
242+ return $ this ->append (['kind ' => 'facts ' , 'facts ' => $ entries ]);
233243 }
234244
235245 /**
@@ -311,13 +321,23 @@ public static function link(string $label, string $href, bool $external = false)
311321 * Appends a vendor-grouped package manifest.
312322 *
313323 * @param string $label Vendor prefix the packages share, shown as the group heading.
314- * @param PackageEntry ...$packages Entries produced by {@see self::package()}.
324+ * @param array<array-key, mixed> ...$packages Entries produced by {@see self::package()}.
325+ *
326+ * @throws InvalidArgumentException if an argument was not built by {@see self::package()}.
315327 *
316328 * @return self New view with the manifest appended.
317329 */
318330 public function manifest (string $ label , array ...$ packages ): self
319331 {
320- return $ this ->append (['kind ' => 'manifest ' , 'label ' => $ label , 'packages ' => array_values ($ packages )]);
332+ $ entries = [];
333+
334+ foreach ($ packages as $ package ) {
335+ self ::assertPackageEntry ($ package );
336+
337+ $ entries [] = $ package ;
338+ }
339+
340+ return $ this ->append (['kind ' => 'manifest ' , 'label ' => $ label , 'packages ' => $ entries ]);
321341 }
322342
323343 /**
@@ -396,13 +416,23 @@ public static function pill(string $label, string $state, bool $enabled): array
396416 /**
397417 * Appends a strip of status pills.
398418 *
399- * @param PillEntry ...$pills Entries produced by {@see self::pill()}.
419+ * @param array<array-key, mixed> ...$pills Entries produced by {@see self::pill()}.
420+ *
421+ * @throws InvalidArgumentException if an argument was not built by {@see self::pill()}.
400422 *
401423 * @return self New view with the pill strip appended.
402424 */
403425 public function pills (array ...$ pills ): self
404426 {
405- return $ this ->append (['kind ' => 'pills ' , 'pills ' => array_values ($ pills )]);
427+ $ entries = [];
428+
429+ foreach ($ pills as $ pill ) {
430+ self ::assertPillEntry ($ pill );
431+
432+ $ entries [] = $ pill ;
433+ }
434+
435+ return $ this ->append (['kind ' => 'pills ' , 'pills ' => $ entries ]);
406436 }
407437
408438 /**
@@ -443,13 +473,23 @@ public static function readout(string $label, string $value, string $caption = '
443473 /**
444474 * Appends a row of headline readout cards.
445475 *
446- * @param ReadoutEntry ...$readouts Entries produced by {@see self::readout()}.
476+ * @param array<array-key, mixed> ...$readouts Entries produced by {@see self::readout()}.
477+ *
478+ * @throws InvalidArgumentException if an argument was not built by {@see self::readout()}.
447479 *
448480 * @return self New view with the readout row appended.
449481 */
450482 public function readouts (array ...$ readouts ): self
451483 {
452- return $ this ->append (['kind ' => 'readouts ' , 'readouts ' => array_values ($ readouts )]);
484+ $ entries = [];
485+
486+ foreach ($ readouts as $ readout ) {
487+ self ::assertReadoutEntry ($ readout );
488+
489+ $ entries [] = $ readout ;
490+ }
491+
492+ return $ this ->append (['kind ' => 'readouts ' , 'readouts ' => $ entries ]);
453493 }
454494
455495 /**
@@ -681,6 +721,96 @@ private function append(array $block): self
681721 return new self ($ this ->summary , [...$ this ->blocks , $ block ], $ this ->toolbar , $ this ->active );
682722 }
683723
724+ /**
725+ * Asserts that an entry was built by {@see self::fact()}.
726+ *
727+ * @param array<array-key, mixed> $entry Entry passed to {@see self::facts()}.
728+ *
729+ * @throws InvalidArgumentException if the entry does not carry the shape the factory produces.
730+ *
731+ * @phpstan-assert FactEntry $entry
732+ */
733+ private static function assertFactEntry (array $ entry ): void
734+ {
735+ if (
736+ ($ entry ['kind ' ] ?? null ) !== 'fact '
737+ || is_string ($ entry ['label ' ] ?? null ) === false
738+ || is_string ($ entry ['value ' ] ?? null ) === false
739+ ) {
740+ throw new InvalidArgumentException (
741+ PanelViewMessage::ENTRY_INVALID ->getMessage ('fact ' , 'fact ' ),
742+ );
743+ }
744+ }
745+
746+ /**
747+ * Asserts that an entry was built by {@see self::package()}.
748+ *
749+ * @param array<array-key, mixed> $entry Entry passed to {@see self::manifest()}.
750+ *
751+ * @throws InvalidArgumentException if the entry does not carry the shape the factory produces.
752+ *
753+ * @phpstan-assert PackageEntry $entry
754+ */
755+ private static function assertPackageEntry (array $ entry ): void
756+ {
757+ if (
758+ ($ entry ['kind ' ] ?? null ) !== 'package '
759+ || is_string ($ entry ['name ' ] ?? null ) === false
760+ || is_string ($ entry ['version ' ] ?? null ) === false
761+ ) {
762+ throw new InvalidArgumentException (
763+ PanelViewMessage::ENTRY_INVALID ->getMessage ('package ' , 'package ' ),
764+ );
765+ }
766+ }
767+
768+ /**
769+ * Asserts that an entry was built by {@see self::pill()}.
770+ *
771+ * @param array<array-key, mixed> $entry Entry passed to {@see self::pills()}.
772+ *
773+ * @throws InvalidArgumentException if the entry does not carry the shape the factory produces.
774+ *
775+ * @phpstan-assert PillEntry $entry
776+ */
777+ private static function assertPillEntry (array $ entry ): void
778+ {
779+ if (
780+ ($ entry ['kind ' ] ?? null ) !== 'pill '
781+ || is_string ($ entry ['label ' ] ?? null ) === false
782+ || is_string ($ entry ['state ' ] ?? null ) === false
783+ || is_bool ($ entry ['enabled ' ] ?? null ) === false
784+ ) {
785+ throw new InvalidArgumentException (
786+ PanelViewMessage::ENTRY_INVALID ->getMessage ('pill ' , 'pill ' ),
787+ );
788+ }
789+ }
790+
791+ /**
792+ * Asserts that an entry was built by {@see self::readout()}.
793+ *
794+ * @param array<array-key, mixed> $entry Entry passed to {@see self::readouts()}.
795+ *
796+ * @throws InvalidArgumentException if the entry does not carry the shape the factory produces.
797+ *
798+ * @phpstan-assert ReadoutEntry $entry
799+ */
800+ private static function assertReadoutEntry (array $ entry ): void
801+ {
802+ if (
803+ ($ entry ['kind ' ] ?? null ) !== 'readout '
804+ || is_string ($ entry ['label ' ] ?? null ) === false
805+ || is_string ($ entry ['value ' ] ?? null ) === false
806+ || is_string ($ entry ['caption ' ] ?? null ) === false
807+ ) {
808+ throw new InvalidArgumentException (
809+ PanelViewMessage::ENTRY_INVALID ->getMessage ('readout ' , 'readout ' ),
810+ );
811+ }
812+ }
813+
684814 /**
685815 * Rejects column headings that are not plain strings.
686816 *
0 commit comments