| |
1 | +# WP44 — Code audit (Agent N, Reproducibility Auditor), 2026-11-03 |
| |
2 | + |
| |
3 | +Scope: read-only audit of the 14 pipeline scripts listed below against `CLAUDE.md` §1/§6, `RESEARCH_PLAN.md` and |
| |
4 | +`protocol/PROTOCOL.md` (v1.0 + amendments A-000…A-004). No file outside `reports/audit/` was modified; the compute node |
| |
5 | +was not touched. Formula and logic checks were replicated on synthetic data on the laptop (DuckDB 1.5.2, numpy 2.4, |
| |
6 | +pandas 3.0, statsmodels 0.14, networkx 3.x); scratch scripts live in `/tmp/wp44_audit/` and are not part of the repo. |
| |
7 | +Machine-readable list: `reports/audit/code_issues.csv` (67 issues: 2 blocking, 20 major, 45 minor). |
| |
8 | + |
| |
9 | +Severity: **blocking** = produces wrong or missing results silently / crashes a pre-registered analysis; |
| |
10 | +**major** = biases an estimand, contradicts the protocol, or loses data in a way that must be fixed before Phase 4 |
| |
11 | +results enter the manuscript; **minor** = hygiene, documentation, fragility, provenance. |
| |
12 | + |
| |
13 | +## 0. Verification checks performed (synthetic data) |
| |
14 | + |
| |
15 | +| Check | Result | |
| |
16 | +|---|---| |
| |
17 | +| Eligibility SQL of `define_corpus.py` with NULL `type` / NULL `source_name` | `eligible` becomes NULL → rows vanish from `WHERE eligible` **and** from every exclusion bucket; PRISMA sequential sum 4 of 6 | |
| |
18 | +| `JOIN … USING (year,type,sf)` with NULL `sf` (`03_citation_impact.py`) | 1 of 3 rows survive | |
| |
19 | +| Gini SQL (`03_citation_impact.py` L35) vs reference `2Σ i·x_(i)/(nΣx) − (n+1)/n`, with ties | identical (0.575 = 0.575); NaN when Σ = 0 | |
| |
20 | +| Directed weighted modularity (`02_category_flows.py`) vs `networkx.community.modularity` | identical to 1e-6 | |
| |
21 | +| Bai–Perron DP (`structural_change.py`) vs brute force, n = 30, m = 2, h = 5 | identical SSR and break indices; BIC and LWZ select m = 2 | |
| |
22 | +| ITS design matrix | `post`, `t_post` (0 at event, +1/month), 11 month dummies — correct | |
| |
23 | +| Placebo candidate months (default window 2018-01 → 2026-06) | 66 candidates, **30 after the true event**; placebo p = 0.015 vs 0.000 with baseline-only placebos | |
| |
24 | +| `features_at` (emerging detection) invariance to columns > T | invariant (look-ahead-safe); trailing z-window = 25 months, not 24 | |
| |
25 | +| Cutoff lists (emerging detection) | train 2020-01…2021-10, test 2023-01…2024-10; h = 24 → last test label month 2026-10, last train label month 2023-10 | |
| |
26 | +| `series_categories` reindex on the boundary month (`run_structural_change.py`) | `ValueError: cannot reindex on an axis with duplicate labels` | |
| |
27 | +| BOCPD update equations (NIG / Student-t) | correct by inspection; unit tests exist | |
| |
28 | +| Embedding normalisation assumed by `semantic_atlas.py` | `embed_corpus.py` uses `normalize_embeddings=True` → cosine via dot product valid | |
| |
29 | + |
| |
30 | +Two documented facts in the repository corroborate findings: `data/metadata/linkage_report.json` shows |
| |
31 | +`dblp_with_arxiv: 0` (dead linkage path), and no script ingests `api_increment/pages/` (window end mismatch). |
| |
32 | + |
| |
33 | +## 1. Per-file findings |
| |
34 | + |
| |
35 | +### ingestion/openalex_snapshot.py — no blocking/major issue |
| |
36 | + |
| |
37 | +| Line | Sev. | Finding | Fix | |
| |
38 | +|---|---|---|---| |
| |
39 | +| 148–157 | minor | A partial download larger than `expected_len` is resumed with `curl -C -` forever; the part stays in `error` on every run | delete `raw_path` on size mismatch before retrying | |
| |
40 | +| 160–164 | minor | SHA-256 recorded but never compared (manifest has no hash) | capture the S3 ETag and compare; label the hash as provenance | |
| |
41 | +| 188–189 | minor | works with NULL `publication_year` excluded from the CS subset without being counted | ledger column `n_null_year` (PRISMA S0) | |
| |
42 | +| 236–239 | minor | `runs` table lacks git commit / environment hash | add both | |
| |
43 | + |
| |
44 | +Design is sound: resumable SQLite ledger, atomic `.part` renames, bad-line counter, slim table for all 465 M works. |
| |
45 | + |
| |
46 | +### ingestion/build_corpus.py — no blocking/major issue |
| |
47 | + |
| |
48 | +| Line | Sev. | Finding | Fix | |
| |
49 | +|---|---|---|---| |
| |
50 | +| 69–76 | minor | `ids["arxiv"]` is not in the OpenAlex schema (dead branch); DOI branch keeps `v\d+` while the URL branch strips it | remove dead branch, strip version everywhere | |
| |
51 | +| 144 | minor | `is_primary` by URL equality → 0 or several primaries when URLs are NULL | compare source id + URLs | |
| |
52 | +| 173–176 | minor | no Parquet metadata (snapshot release, commit) | write key/value metadata | |
| |
53 | + |
| |
54 | +### ingestion/link_sources.py |
| |
55 | + |
| |
56 | +| Line | Sev. | Finding | Fix | |
| |
57 | +|---|---|---|---| |
| |
58 | +| 36, 50–53 | **major** | DBLP→arXiv path yields **0** links (`dblp_with_arxiv: 0`): DBLP CoRR `ee` is now `https://doi.org/10.48550/arXiv.<id>`; the DBLP–arXiv version relation is unavailable to the merge step | derive `arxiv_id` from the DOI `10.48550/arxiv.<id>` too | |
| |
59 | +| 54–60, 67–72, 81–83 | **major** | title+year linkage without the first-author surname required by plan S1 (180 k DBLP, 2.1 k arXiv, 14.9 k ACL links) | add first-author check; report precision by method in the H-1 sample | |
| |
60 | +| 52–83 | minor | `NOT IN (SELECT …)` fragile to NULL keys | `NOT EXISTS` | |
| |
61 | +| 85–118 | minor | report has no `pass` field; committed file is the pass-1 state | add pass + input hashes | |
| |
62 | + |
| |
63 | +### systematic_review/define_corpus.py |
| |
64 | + |
| |
65 | +| Line | Sev. | Finding | Fix | |
| |
66 | +|---|---|---|---| |
| |
67 | +| 52, 55, 65–66 | **BLOCKING** | NULL `type` or NULL `source_name` (non-DBLP, non-arXiv) → `e2_type`/`e7_generic_deposit` NULL → **`eligible` NULL**: silently dropped by every `WHERE eligible` and by every PRISMA bucket; sequential counts do not sum (verified) | `coalesce(type,'')`, `coalesce(source_name,'')`; assert `count(*) WHERE eligible IS NULL = 0`; PRISMA identity test | |
| |
68 | +| 38–39 | major | `pub` CTE: `NOT is_arxiv_doi` is NULL when DOI is NULL → published records without DOI (common for ML conference venues in OpenAlex) can never absorb their preprint → double counting of the most AI-central population | `coalesce(is_arxiv_doi, FALSE)` | |
| |
69 | +| 58, 63–66 | major | preprint flagged `e5_duplicate` even when the matched published record is itself excluded (E1/E2/E7/NULL) → the work disappears entirely | match only against published records passing e1–e4/e7 | |
| |
70 | +| 36–43 | major | merge by exact normalised title only; arXiv-id relation not used; `version_relationship` (plan S2) not produced; retitled VoRs stay duplicated | add arXiv-id pairs + fuzzy fallback; produce `version_relationship`; report duplicate rate | |
| |
71 | +| 50, 56 | major | `first_date = least(...)` lets a year-precision `YYYY-01-01` beat a precise later arXiv date; `date_precision` is defined on `publication_date`, not on the date used for `first_month` | treat year-precision dates as NULL in `least` when a precise date exists; define precision on `first_date` | |
| |
72 | +| 16, 51 | major | `WINDOW_END = 2026-11-02` but data = snapshot 2026-10-14; the API increment is never ingested | ingest the increment or amend the protocol with the effective cut-off | |
| |
73 | +| 51, 84 | minor | NULL `publication_date` counted as E1 | separate reason code | |
| |
74 | +| 33 | minor | `any_value()` nondeterministic for multi-arXiv works | `arg_min(..., created)` | |
| |
75 | +| 65–69 | minor | eligible works with `period='pre'` in yearly but not monthly tables | decide and document | |
| |
76 | +| 67–68 | minor | boundary 2022-11-30 (day) vs ITS event 2022-12-01 (month) | state it | |
| |
77 | + |
| |
78 | +### bibliometrics/01_volume_series.py |
| |
79 | + |
| |
80 | +| Line | Sev. | Finding | Fix | |
| |
81 | +|---|---|---|---| |
| |
82 | +| 22–27 | major | indexing-lag table produced but no last-complete-month / completeness correction derived (plan §6); driver hard-codes `--last-complete 2026-06` | estimate completeness from the lag profile; emit `last_complete_month` JSON used by econometrics and emerging detection | |
| |
83 | +| 22–27 | minor | lag computed on year-precision dates too | restrict to `date_precision='month'` | |
| |
84 | +| 13 | minor | `first_month` vs `date_precision` inconsistency (upstream) | — | |
| |
85 | + |
| |
86 | +### bibliometrics/02_collaboration.py — no formula error found |
| |
87 | + |
| |
88 | +| Line | Sev. | Finding | Fix | |
| |
89 | +|---|---|---|---| |
| |
90 | +| 19–20 | minor | fractional counting = 1/#distinct countries per work, not author-fractional | state the rule | |
| |
91 | +| 21, 24–25 | minor | `share_intl`, `share_company` conditional on affiliation coverage, which drifts by year | add coverage columns | |
| |
92 | +| 14 | minor | retracted and `period='pre'` included | document | |
| |
93 | + |
| |
94 | +### bibliometrics/03_citation_impact.py |
| |
95 | + |
| |
96 | +| Line | Sev. | Finding | Fix | |
| |
97 | +|---|---|---|---| |
| |
98 | +| 26–29 | major | `JOIN USING (year,type,sf)` drops all works with NULL subfield (verified) | `IS NOT DISTINCT FROM` / coalesce | |
| |
99 | +| 4–5, 20–22, 35 | major | `c12` = calendar years y..y+1 (13–24 months), `c36` = y..y+2 (25–36 months); H5 prescribes a 24-month Gini, code uses c36 | rename to calendar-year windows, use y..y+1 for H5, state the publication-month dependence | |
| |
100 | +| 19, 32–37 | major | truncated windows (c36: 2024–2026; c12: 2025–2026) reported without flag | `window_complete` column; NULL when incomplete | |
| |
101 | +| 19 | major | retracted works included (protocol E6 excludes them from impact analyses) | `AND NOT e6_retracted` | |
| |
102 | +| 20–23 | major | citations of the merged preprint version not added to the published node (plan S2 one node per work) | aggregate `counts_by_year` over {id, preprint_work_id} | |
| |
103 | +| 28 | minor | no minimum cell size for NCS normalisation | threshold + fallback | |
| |
104 | +| 35 | — | Gini formula **correct** (verified) | — | |
| |
105 | + |
| |
106 | +### knowledge_graph/01_citation_graph.py |
| |
107 | + |
| |
108 | +| Line | Sev. | Finding | Fix | |
| |
109 | +|---|---|---|---| |
| |
110 | +| 19–26 | major | citations to the merged preprint become external edges → merged node loses inbound citations; propagates to 02 and emerging detection | remap `ref_id` via `duplicate_of` | |
| |
111 | +| 23–26 | minor | dangling references (ids not in slim, e.g. merged ids) dropped silently; `merged_ids` not used | remap + report `n_dangling` | |
| |
112 | +| 31–32 | minor | negative citation ages retained | flag/clip | |
| |
113 | + |
| |
114 | +### knowledge_graph/02_category_flows.py |
| |
115 | + |
| |
116 | +| Line | Sev. | Finding | Fix | |
| |
117 | +|---|---|---|---| |
| |
118 | +| 28–30 | major | inner join on `work_primary` drops unclassified works; shares conditional on classification | `unclassified` pseudo-category + share per year | |
| |
119 | +| 13–17 | — | modularity formula **correct** (directed, weighted; verified) | — | |
| |
120 | +| 52 | minor | no bootstrap CI (P6) | edge-resampling CIs | |
| |
121 | +| 40–41 | minor | `n_month_precision` vs `first_month` inconsistency (upstream) | — | |
| |
122 | + |
| |
123 | +### econometrics/structural_change.py |
| |
124 | + |
| |
125 | +| Line | Sev. | Finding | Fix | |
| |
126 | +|---|---|---|---| |
| |
127 | +| 43–49 | major | placebo months span the whole sample (30/66 post-event); protocol: baseline only; pre-event placebos estimated on a sample containing the true break | restrict to baseline; fit on pre-event data (or control for the true event) | |
| |
128 | +| 61–101 | major | Bai–Perron: DP + BIC/LWZ correct, but no supF/UDmax/sequential tests, no critical values, no seasonal regressors → H3 "5 % level" untestable | add BP tests/critical values or bootstrap p-values; add month dummies | |
| |
129 | +| 32–36 | minor | fixed NW `maxlags=6` (auto rule → 4); design verified | document; lag sensitivity | |
| |
130 | +| 64, 92–100 | minor | break index convention (first of new regime); `h = max(8, 0.15n)` | document | |
| |
131 | +| 104–109 | minor | PELT penalty uses full-sample variance (inflated by the break) | robust scale | |
| |
132 | +| 112–135 | minor | BOCPD data-dependent prior (first 6 obs), fixed hazard; equations correct | document; hazard sensitivity | |
| |
133 | + |
| |
134 | +### econometrics/run_structural_change.py |
| |
135 | + |
| |
136 | +| Line | Sev. | Finding | Fix | |
| |
137 | +|---|---|---|---| |
| |
138 | +| 23–28 | **BLOCKING** | `series_categories` sets `first_month` as index without aggregating; the boundary month 2022-11 has `baseline` and `post` rows → duplicate index → `reindex` raises `ValueError` → **all category-level runs (H3) fail** | `g.groupby("first_month")[col].sum()` | |
| |
139 | +| 36, 44 | minor | `log(n+1)` and Bai–Perron/PELT on unadjusted log series for sparse category series | Poisson/NB ITS robustness; deseasonalise | |
| |
140 | +| 53 | minor | `--first 2018-02` unexplained | document or use 2018-01 | |
| |
141 | +| 63 | minor | `sens_tmp` expression always true; sensitivity runs overwrite each other's `placebo_total.csv`/`fit_total.csv` | name outputs by cut-off | |
| |
142 | +| 56–67 | minor | no provenance stamp in `its_results.json` | add hashes/commit | |
| |
143 | + |
| |
144 | +### taxonomy/classify.py — deterministic; no formula error (tests pass by construction) |
| |
145 | + |
| |
146 | +| Line | Sev. | Finding | Fix | |
| |
147 | +|---|---|---|---| |
| |
148 | +| 108 | minor | `row_number() OVER ()` without `ORDER BY` → chunk composition not reproducible | `ORDER BY id` | |
| |
149 | +| 120–129, 142–143 | minor | shard mode writes no report; outputs not version-stamped (docstring claims it) | stamp ontology version + sha256 | |
| |
150 | +| 146–157 | minor | stale `input_named` symlinks across `--prepare` re-runs | clear in `prepare()` | |
| |
151 | +| 28, 55–57 | minor | regexes compiled without flags; 273/277 patterns carry `(?i)` | compile with `re.I` or lint | |
| |
152 | + |
| |
153 | +### experiments/emerging_detection.py |
| |
154 | + |
| |
155 | +| Line | Sev. | Finding | Fix | |
| |
156 | +|---|---|---|---| |
| |
157 | +| 27–28, 22–23 | major | `KNOWN_SET` empty; H8 lead-time evaluation (`known_set_lead_times.csv`) not implemented | fill `known_set.csv`; implement lead time | |
| |
158 | +| 78, 83–86, 66–74 | major | panel to 2026-10 (incomplete) while econometrics stops at 2026-06: h = 24 test labels truncated; last train label window (2023-10) overlaps test features (from 2023-01) | `T+h ≤ last_complete`; `last_train+h ≤ first_test` | |
| |
159 | +| 31–38 | major (declare) | units = OpenAlex topics (2024 taxonomy that knows which topics emerged); cutoff-specific semantic clusters of the plan not implemented. Features are look-ahead-safe (verified) | implement cutoff-specific clusters; declare the unit-definition look-ahead | |
| |
160 | +| 40–41 | minor | universe selected with full-period counts | per-cutoff selection | |
| |
161 | +| 56–57 | minor | z-window 25 months, docstring 24 | align | |
| |
162 | +| 85 | minor | train cutoffs 2020-01…2021-10, docstring ≤ 2021-12 | align | |
| |
163 | +| 98, 108–113 | minor | unscaled logistic features; pooled AUC; no CIs | scaler pipeline; per-cutoff bootstrap | |
| |
164 | + |
| |
165 | +Label definition (top decile of `log1p(post) − log1p(pre)` among topics with ≥ 20 works in the last 12 months, and |
| |
166 | +post ≥ 2·pre) matches the docstring and uses only (T−h, T] and (T, T+h] (verified). |
| |
167 | + |
| |
168 | +### embeddings/semantic_atlas.py — no blocking/major issue |
| |
169 | + |
| |
170 | +| Line | Sev. | Finding | Fix | |
| |
171 | +|---|---|---|---| |
| |
172 | +| 4, 66 | minor | "stratified sample" is a simple random sample | stratify or fix docstring | |
| |
173 | +| 81–87 | minor | split-half ARI inside one UMAP (not bootstrap; UMAP not refitted) | refit per half | |
| |
174 | +| 49–61 | minor | novelty centroids include year-precision months | restrict to month precision | |
| |
175 | +| 30 | minor | duplicate ids across shards silently overwritten | assert uniqueness | |
| |
176 | +| 41 | minor | baseline/post by calendar year instead of `period` | use `period` | |
| |
177 | +| 67–69, 88 | minor | no version stamp (umap/numba/pynndescent) | record versions | |
| |
178 | + |
| |
179 | +## 2. Cross-cutting observations |
| |
180 | + |
| |
181 | +1. **Preprint–publication merge is only half-implemented.** `define_corpus.py` marks the preprint as a duplicate but no |
| |
182 | + downstream script (citation graph, citation windows, category flows, emerging detection) re-attributes the preprint's |
| |
183 | + references/citations/`counts_by_year` to the surviving node. Under plan S2 ("one node per work") this undercounts |
| |
184 | + early citations and inter-category flows exactly where CS preprints dominate (AI/ML). This is the single most |
| |
185 | + consequential conceptual gap after the two blocking bugs. |
| |
186 | +2. **Date semantics are split across three columns** (`publication_date`, `first_date/first_month`, `date_precision`): |
| |
187 | + monthly series use `first_month` but the precision flag is defined on `publication_date`; yearly tables use |
| |
188 | + `publication_year`. The same work can therefore be counted in different years in yearly vs monthly outputs. |
| |
189 | +3. **Right truncation is handled by a fixed cut-off (2026-06) in econometrics only**; citation windows, emerging-detection |
| |
190 | + labels and the semantic atlas use the data up to the snapshot without a completeness flag. The plan's |
| |
191 | + "indexing-delay profile correction" is not implemented. |
| |
192 | +4. **Effective data end is 2026-10-14**, not 2026-11-02: the API increment is harvested but never ingested. |
| |
193 | +5. **Retracted works** are excluded from impact analyses by the protocol but not by the code. |
| |
194 | +6. **Provenance**: no script stamps the git commit, input hashes or package versions into its outputs; `tests/` covers the |
| |
195 | + pure-python toolkit and the classifier but no DuckDB fixture tests exist for the SQL stages (which is where both |
| |
196 | + blocking bugs sit). Seeds are set where randomness exists (GBM 44, UMAP/HDBSCAN/sample 44); `row_number() OVER ()` |
| |
197 | + and `any_value()` are the only nondeterministic constructs. |
| |
198 | +7. **Inference**: protocol P6 promises bootstrap/HAC uncertainty on all estimates; ITS has HAC, but Bai–Perron, |
| |
199 | + modularity, cross-area shares, Gini and emerging-detection metrics have no intervals. |
| |
200 | + |
| |
201 | +## 3. Prioritised fix list |
| |
202 | + |
| |
203 | +**P0 — before any Phase 4 number is produced** |
| |
204 | +1. `define_corpus.py` L52/L55: `coalesce()` on `type` and `source_name`; add `eligible IS NULL` assertion and a PRISMA |
| |
205 | + identity test (identified = Σ sequential exclusions + after_exclusions). Re-run `define_corpus.py` and every |
| |
206 | + downstream script; compare `prisma_counts.json` before/after. |
| |
207 | +2. `run_structural_change.py` L28: aggregate by `first_month` (`groupby().sum()`), then re-run. |
| |
208 | + |
| |
209 | +**P1 — before results enter the manuscript** |
| |
210 | +3. Merge semantics: `coalesce(is_arxiv_doi, FALSE)` in the `pub` CTE; mark `e5_duplicate` only against eligible |
| |
211 | + published records; add arXiv-id-based pairs and `version_relationship`; remap `ref_id` via `duplicate_of` in |
| |
212 | + `01_citation_graph.py`; sum `counts_by_year` over both versions in `03_citation_impact.py`. |
| |
213 | +4. `03_citation_impact.py`: NULL-safe normalisation join; `window_complete` flag; exclude `e6_retracted`; rename |
| |
214 | + windows to calendar-year semantics and compute H5 on the y..y+1 window. |
| |
215 | +5. `structural_change.py`: baseline-only placebos (fit on pre-event data); add BP supF/sequential tests or bootstrap |
| |
216 | + p-values and month dummies so that H3 can be tested at the stated level. |
| |
217 | +6. Right truncation: derive `last_complete_month` from the lag profile in `01_volume_series.py` and consume it in |
| |
218 | + `run_structural_change.py` and `emerging_detection.py` (labels require `T+h ≤ last_complete`; enforce |
| |
219 | + `last_train+h ≤ first_test`). |
| |
220 | +7. Window end: ingest the API increment or amend the protocol to the effective cut-off 2026-10-14. |
| |
221 | +8. `first_date`/`date_precision` coherence (year-precision dates must not win in `least()`; precision defined on the |
| |
222 | + chosen date); then re-derive `period`, `first_month`. |
| |
223 | +9. `link_sources.py`: arXiv id from the DBLP DOI; first-author check for title+year links; report precision by method. |
| |
224 | +10. `emerging_detection.py`: fill `KNOWN_SET` and implement lead times (H8); implement (or formally drop, by |
| |
225 | + amendment) cutoff-specific semantic clusters and declare the topic-unit look-ahead. |
| |
226 | +11. `02_category_flows.py`: `unclassified` pseudo-category; bootstrap CIs. |
| |
227 | + |
| |
228 | +**P2 — hygiene / reproducibility** |
| |
229 | +12. Provenance stamps (git commit, input sha256, package versions) in every output; `ORDER BY id` in `classify.py` |
| |
230 | + chunking; version stamp in classification outputs; DuckDB fixture tests for `define_corpus.py` and |
| |
231 | + `03_citation_impact.py`; docstring/code alignments (z-window 25 vs 24, cutoffs, "stratified", "bootstrap"); |
| |
232 | + `sens_tmp` naming; `--first 2018-02`; NW lag sensitivity; PELT robust penalty. |
| |
233 | + |
| |
234 | +## 4. Summary |
| |
235 | + |
| |
236 | +Two blocking defects: (i) NULL propagation in the eligibility flags of `define_corpus.py` silently removes works with |
| |
237 | +NULL `type` or NULL `source_name` from the corpus and from the PRISMA counts; (ii) `run_structural_change.py` crashes |
| |
238 | +on every category series because the boundary month appears twice in `category_counts_monthly.csv`. Twenty major |
| |
239 | +issues cluster around four themes: the preprint–publication merge is not propagated to citations/flows; citation windows |
| |
240 | +mislabel calendar-year windows, ignore right truncation and include retracted works; the placebo design includes |
| |
241 | +post-event months and Bai–Perron has no test at the 5 % level; the emerging-detection labels for h = 24 use incomplete |
| |
242 | +months and the preregistered known-emergence evaluation is absent. Verified correct: Gini formula, directed modularity, |
| |
243 | +Bai–Perron dynamic programme and BIC/LWZ penalties, ITS design matrix, BOCPD updates, feature look-ahead safety. |
| |
244 | |