Contributions are welcome. All new code must follow the API grammar below, fixed at the 2026-06-12 design.
API grammar
Three public verbs only:
read_*(ingest),sniff_*(detect/measure),plot_*(visualize). Helpers:is_*,validate_*. Nodetect_,filter_,get_in new exports.Two noun families:
sniff_groups_*operates on stock (groups at a point in time);sniff_trajectory_*operates on flow (temporal objects). The wordtrajectoryis always singular in function names. Thetr::/trNlabel grammar belongs exclusively to the flow partition. Stock plots are alwaysplot_groups_*(the singularplot_group_*prefix is retired).Pipeline-object contract: the first argument of every function is the object produced by the previous pipeline stage. Every
sniff_trajectory_*analysis function takes the flow object. Everyplot_*takes the output of its matchingsniff_*and does not recompute it (the one convenience exception, theplot_trajectory_lines_*family, is named in rule 4).Flexible entry, strict core: the two entry points of the trajectory layer,
sniff_trajectory_dag()andsniff_trajectory_braid(), accept any of the lineage list (sniff_groups_lineage()output),docs_per_group, or (for flow) a dag — resolved by duck-typing in one internal coerceras_trajectory_dag(). Downstream analysis functions demand the exact flow class, with two deliberate conveniences:sniff_trajectory_confluence()shares that entry-point duck-typing (flow, dag, lineage, ordocs_per_group), andplot_trajectory_lines_2d()/plot_trajectory_lines_3d()coerce a loose input to a flow and build the confluence internally whenconfis not supplied.Algorithms are detectors, not arguments: a future detection algorithm is a new
sniff_trajectory_<algo>(dag, ...)that consumes abirddog_dagand returns an object passingvalidate_flow()(same contract:trajectories,tree,graph,docs_per_group,last_year) and inheriting the contract class:class = c("birddog_<algo>", "birddog_flow", "list"). S3 then dispatches every contract method (subset(),print(), future ones) to new detectors for free. Nomethod=dumping-ground argument. “Flow” names the object kind, not the algorithm (stock vs flow: groups are stock, trajectories are flow): every detector returns a flow — the temporal decomposition of the DAG into trajectories plus the confluence tree. The canonical detectorsniff_trajectory_braid()carries the object’s name; its internal algorithm (mutual dominance today) may be improved at a major version without renaming, while named variants pin specific algorithms.validate_flow()is the contract’s authoritative checker.Stable label grammar as public contract:
cNgN(group),y<YYYY><cNgN>(group-year node),tr::<cNgN>(central),trN(absorbed). Documented in?birddogand never changed within a major version.Lifecycle discipline (from 2.0.0 onward): 2.0.0 was the one pre-launch hard break, justified by the absence of users to protect. From 2.0.0 on the rule is strict: new exports start experimental, promoted to stable after one CRAN cycle; anything removed goes through
deprecate_warn()->deprecate_stop()-> deletion, never silently (thelifecyclepackage is added when first needed). Breaking changes only at major versions.Number rules: a noun that is the verb’s direct object takes its natural number (
sniff_groups()finds many;sniff_network()builds one). Layer prefixes have a fixed number:groups_(plural, the stock-layer brand) andtrajectory_(singular, attributive — trajectory flow like citation network). The head noun takes its natural number (_attributes,_hubsplural;_dag,_flow,_lineagesingular;_dynamicsplurale tantum). The plural NEVER distinguishes scope: one-vs-all is an argument (target =,n_routes =) or amap(), never a trailing “s” — the 1.xformation()/formations()trap is banned permanently.
