ossctl_core/protocol/facts.rs
1//! Public wire DTO for the deterministic repo-fact report (`ossctl facts`).
2//!
3//! This is the versioned surface `/oss-init` (config generation) and the
4//! readiness `audit` both read, so they never disagree on maturity or the gated
5//! core (ADR-0001 §3). Its field names and shape are a faithful port of
6//! homebase's `infer-repo-facts.py` JSON — `ecosystems`, `packages`, `has_ci`,
7//! `tags`, `committers_total`/`committers_recent_year`, `inferred_maturity`, and
8//! the rest — because the prose `/oss-init` skill already relies on those exact
9//! names (SCHEMA.md §4 "maturity inference signals").
10//!
11//! Consumers read this document under the CLI's canonical `data` envelope:
12//! `{schema_version, data: <this shape>, warnings}` — the same envelope every
13//! `ossctl --json` command shares (`crate::SCHEMA_VERSION` versions that wire
14//! envelope). Unlike the contract document, the facts report has no
15//! source-level document version of its own (it is *derived*, never authored),
16//! so the envelope's `schema_version` is the single version consumers gate on.
17//!
18//! The report **reuses** [`Ecosystem`] and [`Maturity`] from the canonical
19//! contract model rather than restating their wire strings: facts and the
20//! contract must agree on `"rust"` / `"mvp"` down to the byte, and sharing the
21//! one enum is how that agreement is made structural instead of coincidental.
22
23use serde::Serialize;
24
25use crate::contract::schema::{Ecosystem, Maturity};
26
27/// The deterministic repo-fact report — a pure function of `(repo tree, git
28/// HEAD)`, emitted by `ossctl facts` and consumed by `/oss-init` and `audit`.
29///
30/// Every field is always present (an empty/unborn repo still gets a defined
31/// value for each), mirroring the Python detector's "exit 0 even for an empty
32/// repo" contract.
33// The report mirrors the Python detector's flat boolean signals; grouping them
34// into sub-structs purely to satisfy the bool-count lint would diverge the wire
35// shape from `infer-repo-facts.py` for no consumer benefit.
36#[allow(clippy::struct_excessive_bools)]
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38pub struct Facts {
39 /// The canonicalized repository root the facts were gathered from.
40 pub repo_root: String,
41 /// Whether the root is inside a git work tree.
42 pub is_git: bool,
43 /// Whether the repository has at least one commit (`HEAD` resolves).
44 pub has_commits: bool,
45 /// Detected packaging ecosystems, in canonical order. `[binary]` when no
46 /// package manifest is found (a compiled-artifact-only repo).
47 pub ecosystems: Vec<Ecosystem>,
48 /// One entry per root-level package manifest that names a package (plus
49 /// `Cargo.toml`/`go.mod` always, even as a virtual workspace).
50 pub packages: Vec<Package>,
51 /// Distinct committers across all history (`git shortlog -sne --all`).
52 pub committers_total: usize,
53 /// Distinct committers within the last year — a `production` signal.
54 pub committers_recent_year: usize,
55 /// All tag names in the repository.
56 pub tags: Vec<String>,
57 /// Whether any tag parses as a `SemVer` version (a release signal).
58 pub has_semver_tag: bool,
59 /// Whether a `>=1.0` release exists — a `>=1.0` `SemVer` tag **or** a manifest
60 /// version `>=1.0`. Never probes a registry (that would need the network and
61 /// break reproducibility).
62 pub has_ge_1_0_release: bool,
63 /// Whether a CI configuration is present (`.github/workflows` holding at
64 /// least one entry, or a known single-file CI config).
65 pub has_ci: bool,
66 /// The detected dependency-update bot (`dependabot` / `renovate`), or `null`.
67 pub dependency_bot: Option<String>,
68 /// Whether an `issues/` directory is present.
69 pub has_issues_dir: bool,
70 /// `"spike"` when the README self-labels as pre-release/WIP, else `null`.
71 pub readme_self_label: Option<String>,
72 /// A short project description: the first manifest `description`, else the
73 /// first non-heading README line (both truncated to 120 characters).
74 pub description: Option<String>,
75 /// The raw truth-table signals behind [`Self::inferred_maturity`].
76 pub maturity_signals: MaturitySignals,
77 /// The inferred maturity (SCHEMA.md §4 truth table, tie → `mvp`).
78 pub inferred_maturity: Maturity,
79}
80
81/// One detected package manifest and the name/version parsed from it.
82// `package` is the field name `/oss-init` reads (SCHEMA.md §4); the
83// struct-name-echo lint does not apply to a fixed wire contract.
84#[allow(clippy::struct_field_names)]
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
86pub struct Package {
87 /// The ecosystem this manifest belongs to.
88 pub ecosystem: Ecosystem,
89 /// The manifest filename (e.g. `Cargo.toml`, `package.json`).
90 pub manifest: String,
91 /// The package/crate name, or `null` when the manifest declares none
92 /// (a virtual `Cargo.toml` workspace, or a `go.mod` without a `module`).
93 pub package: Option<String>,
94 /// The declared version, or `null` when absent.
95 pub version: Option<String>,
96}
97
98/// The two maturity truth-table outputs (SCHEMA.md §4). Both can be `false`
99/// (the tie case), which resolves to `mvp`; they are never both `true`
100/// (`production` is checked first).
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102pub struct MaturitySignals {
103 /// `>=2` recent-year committers **and** a `>=1.0` release **and** CI.
104 pub production: bool,
105 /// No CI **and** no `SemVer` tag **and** (single committer **or** a README
106 /// self-label).
107 pub spike: bool,
108}