Pre-1.0. Not intended for public use yet. The API is subject to undocumented change, and these pages may already be wrong about it.

Skip to content

Doctor Checks ​

Introduction ​

See Diagnosing Your Feed for running the doctor and fixing findings.

Command Options ​

OptionEffect
--listprints the check names --only accepts
--only=runs the named checks; repeat it for several
--jsonprints the report as JSON: healthy, count, severity, and each finding's code, severity, message, subject and fix
--stubsprints only the suggested definitions, with their use lines. See Generating Missing Definitions
--fail-on=warning exits non-zero on a warning or an error; error on an error alone. Without it, findings never change the exit status

Available Checks ​

CheckFindsSeverity
grammarrecorded type-and-verb pairs with no headline or no icon, verbs with no Activity Streams 2.0 type, and intransitive verbs recorded with an objecterror · warning · info
aggregatesgroups that formed, or could form, with no group headline. See Group Reachabilityerror · info
tokensgroup headlines that use a token which can differ between the group's memberswarning · info
axesgrouping axes that can hold several verbs, where a group headline names one verb or none existswarning
rolesheadlines that name a role (:object, :target, :context, :origin, :result, :instrument) none of their activities carry, so the placeholder shows as text. :actor over activities that are all anonymous is infoerror · info
actorlessanonymous activities whose verb has no anonymous headlineinfo
reflexiveactivities naming the same entity as actor and objectinfo
verbsrecorded verbs you never registered, registered verbs never recorded, and headlines defined for a type the verb is never recorded on. See Definitionswarning · info
feedsverbs no restricted named feed includes or excludes. See Feed Coveragewarning · info
partiesparty names used but not declared, and declared parties with no activities. See Partieswarning · info
removalsrecorded verbs named like removals (cancel, trash) but are treated as being about their object. See Deleted Modelsinfo
labelsFeedable models whose label is guessed. See Deleted Modelsinfo
inheritedFeedable subclasses deleted through a parent that is not Feedable. See Deleted Modelsinfo
surfaceFeedable models without recorded activities, and ones the enforced morph map cannot name. See Surfacewarning · info
entitiesmodels in a feed role that cannot be resolved: no class, not a model, not Feedable, or the model record is gone. See Entitieserror · warning · info
hydrationFeedable models that load their live model in feedMedia(), and the additional queries per page. See Hydrationinfo
bodythe body types stored, a body with no $body key, and a body type versioned on some records but not otherswarning · info
role_constraintsstored activities whose role types break the declared constraintswarning
keep_latestseveral live activities on a keepLatest() key, or superseded activities on a verb that declares nonewarning · info
retentionactivities past their verb's retention window, and frequent verbs without a retention limit. See Retentionwarning · info
actionsStory class methods that take the request and threw when a job was dispatched, and methods that call request() instead of taking Request. See Actionswarning
recordingrecording switched off (storyfeed.recording.enabled, or stopRecording() at boot), so every publish() saves nothing. An error outside testing, info under iterror · info
tablesmissing package tables. Until feed_tombstones exists, deleted models leave no tombstoneerror
columnsmissing columns in the package tables. Writes that touch them throwerror
manifesta cached story manifest older than your definitions, or definitions that no longer compile while the cache keeps serving themerror
backlogactivities whose entities have no label or link yet. Schedule storyfeed:tricklewarning
hashesgrouping hashes at or beyond the 255-character limit. See Grouping Hasheswarning
shapesmissing or mixed snapshot fingerprints. See Snapshot Shapeswarning · info
groupingactivities with no grouping records, or grouping records without a selected display group. See Groupingwarning
participantsactivities involving() cannot find. storyfeed:participants backfills themwarning
danglingrecords left behind when activities were deleted by a query. They change nothing a feed showsinfo
claimscomposite members still held by a deleted composite. storyfeed:curate --release releases theminfo
freshnessnothing published for doctor.stale_after dayswarning · info
maintenancethe last completed storyfeed:curate and storyfeed:trickle runs, and what they didinfo

Findings ​

Grouping Hashes ​

hashes.truncated warns when a grouping hash reaches or exceeds 255 characters. Shorten the strategy's output, for example by hashing long key parts, then rehash stored activities. Truncated hashes can group unrelated activities together.

Snapshot Shapes ​

shapes.mixed warns when snapshots lack fingerprints or carry mixed fingerprints before a converged maintenance pass. Run storyfeed:trickle to compare snapshots with their models and refresh stale ones. Mixed fingerprints remaining after a pass that rewrites nothing are informational: optional keys can legitimately produce different shapes. No repair is needed for that converged variation.

Group Reachability ​

FindingSeverityMeaning
aggregates.missingerrora type-and-verb group has no headline and is used by a registered feed; Storyfeed falls back to the single-activity headline when valid, or returns no headline
aggregates.latentinfoa group has no headline but is unused by registered feeds; --stubs generates nothing and --fail-on=warning ignores it
aggregates.reachability_unknowninfono feeds are registered, or a feed threw during inspection; all headline gaps are reported as aggregates.missing

Register feeds so the check can distinguish missing headlines they use from those they do not.

Definitions ​

FindingSeverityMeaning
verbs.undeclaredwarninga recorded verb is not registered. Usually a typo; otherwise register it
verbs.deadinfoa registered verb is never recorded. Names the file:line that registered it
grammar.unrecordedinfoa headline is defined for a type and verb that is never recorded, while the verb is recorded on other types. Names the file:line. Usually a copy-paste slip in routes/feed.php, or a definition written ahead of traffic

Feed Coverage ​

FindingSeverityMeaning
feeds.unclassifiedwarningno restricted feed explicitly includes or excludes the verb
feeds.unrestrictedinfoonly a feed declared unrestricted() includes the verb; reported on every run
feeds.unknown_verbwarning; info until the app registers its own verbsa feed names a verb that is neither registered nor recorded; a typo in only() excludes the intended verb
feeds.none_restrictedinfofeeds are registered but none filters verbs
feeds.preset_failedwarninga feed threw during inspection, leaving its verb rules unchecked; this includes define() methods that access constructor values

The check includes registered and recorded verbs. Feeds without only(), except(), or verb() classify none. If your application never calls Storyfeed::feeds(), the check reports no findings. Findings for a named feed include its declaration's file and line.

Declaring an Unrestricted Feed ​

app/Providers/AppServiceProvider.phpboot()
php
use Storyfeed\Facades\Storyfeed;
use Storyfeed\FeedBuilder;

Storyfeed::feeds([
    'portal' => fn (FeedBuilder $feed) => $feed->unrestricted()->live(),
]);

Call unrestricted() to declare that a feed includes every verb. This does not change the query, and callers may still filter it. Verbs covered only by that feed produce feeds.unrestricted (info) instead of feeds.unclassified (warning). An unrestricted declaration cannot also use filters, including verb().

Parties ​

FindingSeverityMeaning
parties.ignoredwarningan actor named a party that Storyfeed::parties() does not declare, so the activity kept its usual actor. Declare the name if it is real
parties.undeclared_actorwarninga verb's ->actor() names an undeclared party: it throws in local and testing and is ignored elsewhere
parties.undeclared_listinfoparties are in use and no list is declared, so any name becomes one
parties.unusedinfoa party has no activities: a typo, or one created ahead of traffic
parties.usedinfoa party, and how many activities it has

Deleted Models ​

FindingSeverityMeaning
removals.unclassifiedinfoa recorded verb is named like a removal, but its activities are treated as being about their object, so they become redundant when the object is deleted. If the verb records the removal, give it an Activity Streams 2.0 Delete, Remove, Undo or Reject type, or declare ->missing() with no roles. If it is about its object, declare ->missing('object'), which silences the finding
labels.guessedinfothe listed models' labels are guessed. Acceptable when the inferred label is suitable; otherwise give each a label in describeFeed(), or in toFeedUsing() for a registered class
inherited.parent_deletesinfoa Feedable subclass, such as FeedablePhoto extends Media, is deleted through a parent that is not Feedable, so its own model events are not dispatched. Reports whether its tombstone is written at the delete (a class in the morph map, or registered with Storyfeed::feedable()) or waits for storyfeed:trickle. An update through the parent waits for the trickle either way

A model's label is also what its tombstone keeps under keepLabel(). Deleted Models covers both.

Feedable Models ​

FindingSeverityMeaning
surface.unwiredwarninga Feedable model has never appeared on an activity, and no headline names its type. Something should publish about it, or the Feedable is left over
surface.unaliasedwarninga Feedable model lacks a required alias. Laravel-wide enforcement throws ClassMorphViolationException; Storyfeed-only enforcement throws FeedableMorphMapViolation
surface.unassessableinfono activities are recorded, so surface.unwired cannot be judged
surface.publisherinfoa class that publishes to the feed

surface.unaliased includes a parent's alias when it finds one. With Laravel-wide morph-map enforcement, the diagnostic explains ClassMorphViolationException:

txt
[App\Models\PriorityOrder] implements Feedable, but the morph map is enforced
and has no alias for it, so publishing anything that names it throws
ClassMorphViolationException. It extends [App\Models\Order], stored as `order`:
return `order` from its getMorphClass() if it should appear as that, or give it
an alias of its own in Relation::enforceMorphMap().

To appear in the feed as its parent, return the parent's alias:

app/Models/PriorityOrder.php
php
<?php

namespace App\Models;

class PriorityOrder extends Order
{
    public function getMorphClass(): string
    {
        return 'order';
    }
}

To give the subclass its own type, register its alias. With Laravel-wide enforcement, add it to Relation::enforceMorphMap().

With Storyfeed::requireFeedableMorphMap(), the exception is FeedableMorphMapViolation. Add the alias through Relation::morphMap(); you do not need to enable Laravel-wide enforcement:

app/Providers/AppServiceProvider.phpboot()
php
use App\Models\PriorityOrder;
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap(['priority-order' => PriorityOrder::class]);

A model without an aliased parent needs its own alias in either mode.

Entities ​

Findings include the role, alias, class, affected activity count, and example activity IDs.

FindingSeverityMeaning
entities.auth_modelwarningthe authentication model does not implement Feedable, so every activity published during a request has an actor with no label or link. Skipped when actor_resolver is set
entities.unresolvableerrorthe alias resolves to no class: no morph map entry, and no class by that name
entities.not_modelerrorthe alias resolves to a class that is not an Eloquent model
entities.unfeedableerrorthe alias resolves to a model without Feedable. Implement Feedable, then run storyfeed:trickle
entities.missingwarningthe model is Feedable, but the row is gone or hidden by a global scope. Checked on the 50 most recent affected activities per role and alias. storyfeed:trickle first attempts to tombstone missing entities and repoint their activities. --prune removes activities only when roles remain unresolved afterward
entities.opaqueinfothe model's table could not be queried

Tombstone discovery checks without global scopes, so a live row hidden by a scope is not treated as deleted. Explicit forgetWhenMissing rules are a separate deletion policy; see Deleted Models.

Affected entities display without labels or links. Existing entities whose labels are not cached yet are reported by backlog.

Hydration ​

The hydration check reports models loaded by feedMedia() and the additional queries required per page.

FindingSeverityMeaning
hydration.modelinfoa class loads its model in feedMedia() for the listed feeds, adding one query per class per page; also reports when hydration.enabled is off and the call returns null
hydration.pageinfodistinct model classes loaded for the 30 most recent activities, and the resulting query count
hydration.opaqueinfofeedMedia() threw, so model loading could not be checked

Role Constraints ​

FindingSeverityMeaning
role_constraints.violatedwarningstored activities have role types outside the declared constraints. Empty roles and deleted models are skipped. The activities stay in the feed

Retention ​

FindingSeverityMeaning
retention.backlogwarningactivities are over a day past their retention period; preview with storyfeed:prune --pretend before deleting them. Often caused by changed retention or an unscheduled pruning command
retention.unboundedinfoa verb recorded at least 10,000 times in 30 days has no retention limit; excludes verbs declaring keepForever()

Actions ​

FindingSeverityMeaning
actions.carry_failedwarninga Story class method that takes the Request threw when a job was dispatched. The job still ran, and published with the actor it would otherwise have had
actions.request_helperwarninga Story class method reads the request through request() or the Request facade instead of taking Illuminate\Http\Request $request. It accesses the request only when stories are compiled, never when an activity publishes. Take the Request as a parameter

Grouping ​

FindingSeverityMeaning
grouping.ungroupedwarningactivities have no grouping records and can only appear individually; run storyfeed:curate --rehash
grouping.uncuratedwarningactivities have grouping records, but no group has been selected for display; run storyfeed:curate

Generating Missing Definitions ​

Use --stubs to generate definitions suggested by findings: headlines, icons, anonymous headlines, group headlines, and keepLatest() declarations.

StubCondition
uncommentedthe doctor can generate a past-tense headline using tokens shared by every activity
commented out with an explanationthe definition needs your input: an icon, an uncertain past tense such as ship, a key covering all verbs, or a group containing several verbs. The finding remains until you complete it
absentroles findings require rewriting a headline; aggregates.latent headlines are unused by registered feeds

Headlines for groups of one type are declared on that type. Groups containing several types use verb-level headlines without a type-specific noun. For one user placing three orders and several users ordering for the same customer:

routes/feed.php
php
use App\Models\Order;
use Storyfeed\Facades\Story;
use Storyfeed\Grouping\GroupBuilder;

Story::verb('place')->grouped(fn (GroupBuilder $group) => $group->actors(':actors placed :objects'));
Story::for(Order::class)->verb('place')->grouped(fn (GroupBuilder $group) => $group->repeat(':actor placed :objects'));

The actors stub contains two lists. Rewrite it to use one before adding it, as described in Aggregation.

Output omits headings and counts so it can be piped. // Nothing to author means no definitions can be generated, even if findings exist. With --json, each fix's definition contains the same generated line.

Released under the MIT License.