Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62741343ee | ||
|
|
0c7c7eb299
|
||
|
|
7274ce3bfe
|
||
|
|
24e86ed598
|
||
|
|
1ec20174b7
|
||
|
|
2e317d3a0f
|
||
|
|
f7c606984f | ||
|
|
9617b86a26
|
||
|
|
224e5d0530 |
@@ -3,6 +3,7 @@
|
||||
^\.Rproj\.user$
|
||||
^_pkgdown\.yml$
|
||||
^docs$
|
||||
^pm$
|
||||
^Meta$
|
||||
^doc$
|
||||
^pkgdown$
|
||||
|
||||
@@ -39,5 +39,17 @@ jobs:
|
||||
echo "PAT_GH is unset -- add it under Settings > Actions > Secrets." >&2
|
||||
exit 1
|
||||
fi
|
||||
# On a tag-triggered run, checkout materializes refs/tags/<tag> as a
|
||||
# LIGHTWEIGHT tag at the commit SHA -- the annotated tag object Gitea
|
||||
# holds is never fetched. Mirroring that strips the annotation, and the
|
||||
# NEXT run on main (which does fetch the real object) is then rejected
|
||||
# with "already exists" trying to correct it, because git will not
|
||||
# clobber an existing tag. That is why v0.4.0 failed to mirror.
|
||||
#
|
||||
# Re-fetch canonical tag objects from Gitea first. --force here rewrites
|
||||
# LOCAL tag refs only; it is not a force push and does not weaken the
|
||||
# non-force guarantee on main documented above.
|
||||
git fetch --tags --force origin
|
||||
|
||||
git push "https://x-access-token:${PAT_GH}@github.com/civilytics/uscogdata.git" \
|
||||
HEAD:refs/heads/main --tags
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
.Ruserdata
|
||||
*.Rproj
|
||||
inst/doc
|
||||
# pkgdown output. Compass used to keep its files in docs/pm/ and
|
||||
# docs/decisions/, which forced this to be written as children with two
|
||||
# re-includes -- git cannot re-include anything beneath an excluded directory.
|
||||
# Compass lives in pm/ now, so the whole directory can be excluded again.
|
||||
docs/
|
||||
/doc/
|
||||
/Meta/
|
||||
@@ -12,3 +16,6 @@ docs/
|
||||
|
||||
# SDD working artifacts (ledger, briefs, review packages) — plans/ stays tracked
|
||||
.superpowers/sdd/
|
||||
.compass-cache/
|
||||
# roborev snapshots
|
||||
/.roborev/
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# roborev configuration, initialised by compass.
|
||||
# Reviews are queued to a background daemon -- they never block a commit.
|
||||
|
||||
post_commit_review = 'commit'
|
||||
excluded_commit_patterns = ['WIP', 'chore:', 'chore(', 'docs:', 'Merge ']
|
||||
|
||||
review_guidelines = '''
|
||||
# --- compass:begin (generated -- edit the sources, not this) ---
|
||||
- Prefer returning new values to mutating arguments in place. A function that edits
|
||||
its caller's object is a bug waiting for a second caller.
|
||||
- Validate at system boundaries -- user input, API responses, file contents, config.
|
||||
Fail fast with a message naming the field and the file.
|
||||
- Never swallow an error. Handle it or let it propagate; a bare catch that continues
|
||||
is worse than a crash.
|
||||
- No hardcoded secrets, tokens, or credentials, and no secrets in log output or error
|
||||
messages.
|
||||
- Parameterise every query. String-built SQL is a defect even when the input looks safe.
|
||||
- Keep functions under roughly 50 lines and files under roughly 400. Flag nesting
|
||||
deeper than four levels.
|
||||
- No magic numbers or hardcoded paths -- name them as constants or read them from config.
|
||||
- New behaviour needs a test. A bug fix needs a test that fails without the fix.
|
||||
- Prose a person reads -- an issue title or body, a journal entry, a decision record,
|
||||
the narrative on the status board -- names the action or the thing, not the shape of
|
||||
the machinery. Flag "gate", "seam", "surface area", "load-bearing", "first-class",
|
||||
"primitive", "blast radius". A project's own defined vocabulary is not the target.
|
||||
- Use the native pipe `|>`, not magrittr `%>%`.
|
||||
- snake_case for objects and functions; UPPER_SNAKE for constants. Never use `.` as a
|
||||
word separator in a function name -- it collides with S3 dispatch.
|
||||
- Validate arguments at the top of exported functions with `stopifnot()` or an explicit
|
||||
check, and say which argument was wrong.
|
||||
- Never `setDT()`, `set()`, or otherwise modify by reference a data.table the caller
|
||||
still owns. `as.data.table()` copies; use it.
|
||||
- Prefer `vapply()` to `sapply()` -- `sapply()` silently returns a list when the type
|
||||
varies, which turns a type error into a downstream mystery.
|
||||
- Use `seq_len(n)` / `seq_along(x)`, never `1:n`, which iterates backwards when n is 0.
|
||||
- Compare strings with `==` only after checking for NA; use `identical()` for scalars
|
||||
where NA would be wrong.
|
||||
- Do not call `library()` inside package or module files; attach packages in scripts and
|
||||
test helpers only.
|
||||
- Namespace-qualify calls into other packages (`stats::sd`) in code that is sourced.
|
||||
- Every exported function needs roxygen with `@param` for each argument (type, meaning,
|
||||
and why the default is what it is) and `@return`. Add `@examples` for exported API.
|
||||
- Declare dependencies in DESCRIPTION. Prefer base R or an existing dependency over
|
||||
adding a new one; a package with zero hard deps is worth keeping that way.
|
||||
- Signal errors with `stop()` carrying a condition class, so callers can catch the kind
|
||||
rather than matching on message text.
|
||||
- Keep internals internal. Export only what a user needs; an accidentally exported
|
||||
helper becomes an API you have to keep.
|
||||
- Tests use testthat edition 3. Each test is self-sufficient -- no reliance on state
|
||||
left by an earlier test or on a fixture built elsewhere in the file.
|
||||
- Prefer duplication in tests over a helper that hides what is being asserted.
|
||||
- Every verb calls .ensure_session() first, then queries via DBI::dbGetQuery().
|
||||
- A verb's return value is always a tbl_df carrying a provenance attribute.
|
||||
- govid inputs always go through .coerce_govid_input(); it accepts a character vector or a data frame.
|
||||
- SQL has two layers: view definitions are numbered .sql files in inst/sql/ registered by .register_views(); query construction is inline sprintf() in R. Add a view as a file; build a query in R.
|
||||
- No arrow dependency -- DuckDB reads parquet natively.
|
||||
- withr is Suggests-only and must appear in tests alone.
|
||||
- Tests must pass offline against the bundled fixture; tests/testthat/setup.R sets USCOGDATA_URL for that.
|
||||
# --- compass:end ---
|
||||
'''
|
||||
+136
-54
@@ -84,6 +84,21 @@
|
||||
#' @return List of `list(recipe_id, label, available_years, hint,
|
||||
#' ig_recipe_id, trigger, suppressed_amount, suppressed_years,
|
||||
#' suppressed_codes)`, possibly empty.
|
||||
#'
|
||||
#' Decomposed (Issue #33) into three extracted helpers to stay within the
|
||||
#' project's "functions under 50 lines" convention:
|
||||
#' \itemize{
|
||||
#' \item `.query_candidate_recipes()` -- candidate recipe lookup by
|
||||
#' category/subtype scope + M/L exclusion.
|
||||
#' \item `.query_recipe_meta()` -- metadata (label, year spans).
|
||||
#' \item `.query_covered_years()` -- Path 1 gap-year coverage via the
|
||||
#' recipe's own generic join.
|
||||
#' }
|
||||
#' The for-loop that merges covered-years + suppressed-components into
|
||||
#' suggestion objects stays inline here because it interleaves
|
||||
#' empty_hit/supp_hit precedence with field assembly. Likewise kept inline:
|
||||
#' the M/L-exclusion design-comment block and the final
|
||||
#' `.attach_ig_counterparts()` call.
|
||||
#' @noRd
|
||||
.build_suggestions <- function(con, cohort, years, category, result, basis,
|
||||
flow_prefixes, long_view,
|
||||
@@ -111,28 +126,8 @@
|
||||
# by `category` (`.ALL_CATEGORIES` is never a row in
|
||||
# `summary_categories.category`, so a category-keyed sub-select always
|
||||
# came back empty here). The M/L exclusion below is unchanged either way.
|
||||
candidate_scope_sql <- if (isTRUE(all_categories)) {
|
||||
sprintf(
|
||||
"SELECT DISTINCT item_code FROM summary_categories WHERE %s IN (%s)",
|
||||
subtype_col, .sql_lit_chr(subtype_scope)
|
||||
)
|
||||
} else {
|
||||
sprintf(
|
||||
"SELECT DISTINCT item_code FROM summary_categories WHERE category IN (%s)",
|
||||
.sql_lit_chr(category)
|
||||
)
|
||||
}
|
||||
candidates <- DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE component_code IN (
|
||||
%s
|
||||
)
|
||||
AND recipe_id NOT IN (
|
||||
SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE LEFT(component_code, 1) IN ('M', 'L')
|
||||
)",
|
||||
candidate_scope_sql
|
||||
))$recipe_id
|
||||
candidates <- .query_candidate_recipes(con, category, all_categories,
|
||||
subtype_col, subtype_scope)
|
||||
if (length(candidates) == 0L) return(list())
|
||||
|
||||
result_years <- if (is.null(result) || nrow(result) == 0L) {
|
||||
@@ -164,36 +159,11 @@
|
||||
|
||||
if (length(gap_years) == 0L && nrow(supp) == 0L) return(list())
|
||||
|
||||
meta <- tibble::as_tibble(DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT recipe_id, any_value(label) AS label,
|
||||
MIN(year_min) AS year_min, MAX(year_max) AS year_max
|
||||
FROM harmonization_recipes
|
||||
WHERE recipe_id IN (%s)
|
||||
GROUP BY recipe_id",
|
||||
.sql_lit_chr(candidates)
|
||||
)))
|
||||
meta <- .query_recipe_meta(con, candidates)
|
||||
|
||||
# Path 1 (unchanged): (recipe, year) pairs the recipe's own generic join
|
||||
# covers for this government, restricted to the gap years.
|
||||
covered <- if (length(gap_years) == 0L) {
|
||||
data.frame(recipe_id = character(0), year = integer(0))
|
||||
} else {
|
||||
DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT DISTINCT r.recipe_id, l.year
|
||||
FROM long l
|
||||
JOIN harmonization_recipes r
|
||||
ON l.item_code = r.component_code
|
||||
AND l.year BETWEEN r.year_min AND r.year_max
|
||||
AND (r.gov_type_scope = 'all'
|
||||
OR (r.gov_type_scope = 'state' AND l.type = 0)
|
||||
OR (r.gov_type_scope = 'local' AND l.type BETWEEN 1 AND 3))
|
||||
WHERE r.recipe_id IN (%s)
|
||||
AND %s
|
||||
AND l.year IN (%s)",
|
||||
.sql_lit_chr(candidates), .cohort_sql(cohort, "l.canonical_govid"),
|
||||
paste(gap_years, collapse = ",")
|
||||
))
|
||||
}
|
||||
covered <- .query_covered_years(con, candidates, cohort, gap_years)
|
||||
|
||||
suggestions <- list()
|
||||
for (rid in candidates) {
|
||||
@@ -227,6 +197,122 @@
|
||||
.attach_ig_counterparts(con, suggestions, flow_prefixes)
|
||||
}
|
||||
|
||||
#' Query candidate harmonization recipe IDs for a coverage-gap suggestion.
|
||||
#'
|
||||
#' Selects recipes whose component codes fall within the requested scope
|
||||
#' (category or subtype allowlist), excluding any recipe that is ITSELF an
|
||||
#' intergovernmental (M/L) recipe -- i.e. every one of its own component
|
||||
#' codes is M/L-prefixed. Without this exclusion, a category whose
|
||||
#' summary_categories rows span both a Direct family (e.g. E04/E05,
|
||||
#' "Corrections") and its M/L counterpart (M04/M05) makes the M/L recipe
|
||||
#' itself a raw top-level candidate for a plain `cog_spending()` call --
|
||||
#' following that hint would silently return intergovernmental dollars
|
||||
#' under `expenditure_concept = "direct"` provenance.
|
||||
#'
|
||||
#' In all-categories mode (`all_categories = TRUE`) the inner sub-select is
|
||||
#' scoped by `subtype_col`/`subtype_scope` -- the same allowlist
|
||||
#' `.build_verb_sql()` applies as a WHERE predicate to make the summed
|
||||
#' result a *concept* (see R/spending.R), not by `category`.
|
||||
#' `.ALL_CATEGORIES` ("All Categories") is never itself a row in
|
||||
#' `summary_categories.category`, so a category-keyed sub-select always
|
||||
#' returns zero candidates and silently disables signposting.
|
||||
#'
|
||||
#' @param con Active DuckDB connection.
|
||||
#' @param category Category name, or `NULL`.
|
||||
#' @param all_categories `TRUE` when the caller used `.ALL_CATEGORIES`.
|
||||
#' @param subtype_col Name of the summary_categories subtype column to
|
||||
#' scope by when `all_categories = TRUE`; ignored otherwise.
|
||||
#' @param subtype_scope Character vector of subtype values to scope by
|
||||
#' when `all_categories = TRUE`; ignored otherwise.
|
||||
#' @return Character vector of recipe IDs (possibly empty).
|
||||
#' @noRd
|
||||
.query_candidate_recipes <- function(con, category, all_categories = FALSE,
|
||||
subtype_col = NULL,
|
||||
subtype_scope = NULL) {
|
||||
candidate_scope_sql <- if (isTRUE(all_categories)) {
|
||||
sprintf(
|
||||
"SELECT DISTINCT item_code FROM summary_categories WHERE %s IN (%s)",
|
||||
subtype_col, .sql_lit_chr(subtype_scope)
|
||||
)
|
||||
} else {
|
||||
sprintf(
|
||||
"SELECT DISTINCT item_code FROM summary_categories WHERE category IN (%s)",
|
||||
.sql_lit_chr(category)
|
||||
)
|
||||
}
|
||||
|
||||
DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE component_code IN (
|
||||
%s
|
||||
)
|
||||
AND recipe_id NOT IN (
|
||||
SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE LEFT(component_code, 1) IN ('M', 'L')
|
||||
)",
|
||||
candidate_scope_sql
|
||||
))$recipe_id
|
||||
}
|
||||
|
||||
#' Query gap-year coverage: which (recipe, year) pairs the recipe's own
|
||||
#' generic join covers for this government, restricted to `gap_years`.
|
||||
#'
|
||||
#' This is Path 1 of a suggestion (unchanged): it finds recipes whose
|
||||
#' component codes' generic join produces at least one row for this
|
||||
#' government in each gap year -- i.e. the category returned nothing in
|
||||
#' that year but a recipe would fill it.
|
||||
#'
|
||||
#' @param con Active DuckDB connection.
|
||||
#' @param candidates Character vector of recipe IDs to check coverage for.
|
||||
#' @param cohort The verb's cohort object (see `.make_cohort()`), rendered
|
||||
#' into the govid predicate on the joined `long` scan via `.cohort_sql()`.
|
||||
#' @param gap_years Integer vector of requested years absent from the
|
||||
#' result.
|
||||
#' @return Data frame with columns `recipe_id` (character) and `year`
|
||||
#' (integer). Returns an empty data frame (`recipe_id = character(0)`,
|
||||
#' `year = integer(0)`) when `gap_years` is empty, so callers can safely
|
||||
#' reference `$recipe_id`.
|
||||
#' @noRd
|
||||
.query_covered_years <- function(con, candidates, cohort, gap_years) {
|
||||
if (length(gap_years) == 0L) {
|
||||
return(data.frame(recipe_id = character(0), year = integer(0)))
|
||||
}
|
||||
DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT DISTINCT r.recipe_id, l.year
|
||||
FROM long l
|
||||
JOIN harmonization_recipes r
|
||||
ON l.item_code = r.component_code
|
||||
AND l.year BETWEEN r.year_min AND r.year_max
|
||||
AND (r.gov_type_scope = 'all'
|
||||
OR (r.gov_type_scope = 'state' AND l.type = 0)
|
||||
OR (r.gov_type_scope = 'local' AND l.type BETWEEN 1 AND 3))
|
||||
WHERE r.recipe_id IN (%s)
|
||||
AND %s
|
||||
AND l.year IN (%s)",
|
||||
.sql_lit_chr(candidates), .cohort_sql(cohort, "l.canonical_govid"),
|
||||
paste(gap_years, collapse = ",")
|
||||
))
|
||||
}
|
||||
|
||||
#' Query recipe metadata: labels and year spans for a set of candidate
|
||||
#' recipes.
|
||||
#'
|
||||
#' @param con Active DuckDB connection.
|
||||
#' @param candidates Character vector of recipe IDs to look up.
|
||||
#' @return Tibble with columns `recipe_id`, `label`, `year_min` (int), and
|
||||
#' `year_max` (int).
|
||||
#' @noRd
|
||||
.query_recipe_meta <- function(con, candidates) {
|
||||
tibble::as_tibble(DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT recipe_id, any_value(label) AS label,
|
||||
MIN(year_min) AS year_min, MAX(year_max) AS year_max
|
||||
FROM harmonization_recipes
|
||||
WHERE recipe_id IN (%s)
|
||||
GROUP BY recipe_id",
|
||||
.sql_lit_chr(candidates)
|
||||
)))
|
||||
}
|
||||
|
||||
#' Attach `ig_recipe_id` to each suggestion: the intergovernmental-expenditure
|
||||
#' recipe (an M-to-local or L-to-state recipe) whose component codes cover
|
||||
#' exactly the same set of function suffixes as the firing recipe's own
|
||||
@@ -272,7 +358,7 @@
|
||||
#' `R/basis.R`). This blocks a recipe surfaced through a mis-scoped
|
||||
#' category from ever reaching the M/L search, e.g. `cog_spending()`'s
|
||||
#' flow_prefixes are `c("E","F","G")`, which `ig_federal_b47_wide`'s own
|
||||
#' `"B"` is not part of.
|
||||
#' "B" is not part of.
|
||||
#' 2. `own_prefix %in% c("E","F","G")`: M/L only ever pairs with the
|
||||
#' DIRECT-expenditure family, never with revenue (`cog_revenue()`'s
|
||||
#' flow_prefixes already fold B/C/D in as ordinary revenue -- there is
|
||||
@@ -280,10 +366,6 @@
|
||||
#' adds one for spending) and never with ANOTHER M/L recipe (without
|
||||
#' this check, `ige_local_m47_wide` would wrongly match sibling
|
||||
#' `ige_state_l47_wide` on their shared {"47","94"} suffix set).
|
||||
#' Condition 1 alone does not catch this: under `cog_revenue()`,
|
||||
#' `ig_federal_b47_wide`'s own `"B"` IS inside revenue's own
|
||||
#' `flow_prefixes`, so only this second, family-specific check blocks
|
||||
#' the search.
|
||||
#' @noRd
|
||||
.attach_ig_counterparts <- function(con, suggestions, flow_prefixes) {
|
||||
if (length(suggestions) == 0L) return(suggestions)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
# Project journal
|
||||
|
||||
Append-only, newest first. **Entries are never edited** — the value of this file is
|
||||
that it records what was believed at the time, including the parts that turned out
|
||||
wrong. Where things stand *today* is in `STATUS.md`, which is generated.
|
||||
|
||||
Four lines per entry. The analysis belongs in the issue or the decision record; this
|
||||
file carries the reasoning and the pointers.
|
||||
|
||||
- **Why** — the driver. The one line git cannot reconstruct later.
|
||||
- **Obligates** — issues this change created elsewhere. Numbers, not prose.
|
||||
- **Refs** — commits, issues, decision records.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,67 @@
|
||||
# Project status
|
||||
|
||||
> Between the compass markers is generated. Edit the sources, not this.
|
||||
|
||||
<!-- compass:begin -->
|
||||
<!-- compass:board -->
|
||||
|
||||
## Where this stands
|
||||
|
||||
uscogdata is at 0.4.0 and its public surface is settled: the query verbs, the cohort
|
||||
predicates added in this release, and the provenance contract every verb returns.
|
||||
|
||||
The six open issues split cleanly. Two are API work carried out of the #9 review pass
|
||||
and deliberately deferred there rather than fixed in that branch. Three concern the
|
||||
corpus layer, and the largest of them, partition-level caching, was named the single
|
||||
highest-leverage change on the remote path before being deferred. One, the
|
||||
data-correction intake (#52), is a decision rather than a task: it was parked during
|
||||
the 0.3.0 design, and the API announcement waits on it, because without it the corpus
|
||||
cannot make the "traceable and correctable" claim that most distinguishes it from
|
||||
Census's own files.
|
||||
|
||||
Nothing here is blocked on anything else, so the ordering is a judgement about value
|
||||
rather than a dependency graph.
|
||||
|
||||
Compass's own files moved out of `docs/` this session. They were sitting inside
|
||||
pkgdown's output directory, and `pkgdown::clean_site()` deletes every top-level entry
|
||||
there except `CNAME` and `dev` — asked directly, it listed `docs/pm` and
|
||||
`docs/decisions` among the 28 it would remove, with the guard that would have stopped
|
||||
it satisfied by `docs/pkgdown.yml`. They are in `pm/` now. Nothing was lost: the
|
||||
journal had no entries and there were no decision records yet, which made this the
|
||||
cheapest moment to move. The `.gitignore` workaround that re-included two children of
|
||||
an excluded `docs/` is gone with it.
|
||||
|
||||
## Ready to work on next
|
||||
|
||||
- **#34** cog_revenue() offers expenditure recipes as suggestions: scope the candidate query by category_type · `ws/api` — nothing is blocking it; something is currently wrong
|
||||
- **#36** n_units_reporting is category-conditional and cannot be read as a response rate · `ws/corpus` — nothing is blocking it; owed work from an earlier change
|
||||
- **#2** Extend population data to be households as an alternate spending denominator · `ws/corpus` — nothing is blocking it
|
||||
- **#33** Decompose .build_suggestions() (106 lines) into named helpers · `ws/api` — nothing is blocking it
|
||||
- **#52** Release 11/11: design the data-correction intake (deferred; gates the API announcement) · `ws/corpus` — nothing is blocking it
|
||||
- **#64** Partition-level caching: R/cache.R is still a stub, and the remote path pays for it every session · `ws/corpus` — nothing is blocking it
|
||||
|
||||
## Workstreams
|
||||
|
||||
| Stream | Commits since | Open | Debt | Owes docs |
|
||||
|---|---|---|---|---|
|
||||
| Query verbs and results | 77 | 2 | 0 | no |
|
||||
| Corpus, mirror, provenance | 39 | 4 | 1 | no |
|
||||
| Vignettes and guides | 34 | 0 | 0 | **yes** |
|
||||
|
||||
## CI
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>Dependency graph and detail</summary>
|
||||
|
||||
_Nothing blocks anything else, so there is no graph to draw._
|
||||
|
||||
- Marker: `none` (no journal entry yet)
|
||||
- Commits since: 165
|
||||
- Open issues: 6
|
||||
|
||||
</details>
|
||||
|
||||
<!-- compass:end -->
|
||||
@@ -0,0 +1,44 @@
|
||||
[project]
|
||||
name = "uscogdata"
|
||||
forge = "Civilytics/uscogdata"
|
||||
|
||||
# Three strands that go stale independently: what the verbs return, what the
|
||||
# corpus is and how it is mounted, and how both are explained to a reader.
|
||||
|
||||
[[workstream]]
|
||||
id = "api"
|
||||
title = "Query verbs and results"
|
||||
paths = [
|
||||
"R/revenue.R", "R/spending.R", "R/balances.R", "R/peers.R", "R/search.R",
|
||||
"R/categories.R", "R/recipes.R", "R/rollup.R", "R/explain.R", "R/basket.R",
|
||||
"R/suggestions.R", "R/suppression.R", "R/complete.R", "R/cohort.R",
|
||||
"R/basis.R", "R/adjust.R", "R/pagination.R",
|
||||
]
|
||||
docs = ["vignettes/*.Rmd", "README.md"]
|
||||
|
||||
[[workstream]]
|
||||
id = "corpus"
|
||||
title = "Corpus, mirror, provenance"
|
||||
paths = [
|
||||
"R/manifest.R", "R/mirror.R", "R/cache.R", "R/session.R", "R/provenance.R",
|
||||
"R/coverage.R", "R/config.R", "R/views.R", "R/series_breaks.R",
|
||||
"R/balance_caveats.R", "R/zzz.R", "data-raw/**", "inst/sql/**",
|
||||
]
|
||||
docs = ["vignettes/*.Rmd", "NEWS.md"]
|
||||
|
||||
[[workstream]]
|
||||
id = "docs"
|
||||
title = "Vignettes and guides"
|
||||
paths = ["vignettes/**", "README.md", "_pkgdown.yml", "NEWS.md"]
|
||||
docs = []
|
||||
|
||||
[roborev]
|
||||
project_guidelines = [
|
||||
"Every verb calls .ensure_session() first, then queries via DBI::dbGetQuery().",
|
||||
"A verb's return value is always a tbl_df carrying a provenance attribute.",
|
||||
"govid inputs always go through .coerce_govid_input(); it accepts a character vector or a data frame.",
|
||||
"SQL has two layers: view definitions are numbered .sql files in inst/sql/ registered by .register_views(); query construction is inline sprintf() in R. Add a view as a file; build a query in R.",
|
||||
"No arrow dependency -- DuckDB reads parquet natively.",
|
||||
"withr is Suggests-only and must appear in tests alone.",
|
||||
"Tests must pass offline against the bundled fixture; tests/testthat/setup.R sets USCOGDATA_URL for that.",
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
# Decisions
|
||||
|
||||
One file per decision, numbered and immutable. A decision that changes is superseded
|
||||
by a new record, never edited in place — the old reasoning is the point.
|
||||
|
||||
The table below is **generated** by `compass:decide`. Do not hand-edit it.
|
||||
|
||||
<!-- compass:begin decisions -->
|
||||
| # | Date | Decision | Status |
|
||||
|---|---|---|---|
|
||||
| — | — | *No decisions recorded yet.* | — |
|
||||
<!-- compass:end decisions -->
|
||||
Reference in New Issue
Block a user