[v1 scaffold · DRAFT] State of Equipment Management 2026 — anonymized aggregate extraction kit - #2570
Draft
carlosvirreira wants to merge 4 commits into
Draft
[v1 scaffold · DRAFT] State of Equipment Management 2026 — anonymized aggregate extraction kit#2570carlosvirreira wants to merge 4 commits into
carlosvirreira wants to merge 4 commits into
Conversation
… kit
V1 scaffold for the data-extraction script that produces the anonymized
aggregates feeding the public State of Equipment Management 2026 report
on shelf.nu/reports/state-of-equipment-management-2026.
This is the data-team half of a two-repo effort. The companion PR on
shelf-nu/website-v2 scaffolds the report page, the typed data structure
the script's output JSON feeds into, and the distribution materials.
apps/webapp/scripts/state-of-em-2026.ts (NEW)
- Orchestrator entry point. Mirrors the structure of
seed-reporting-demo.ts: parses CLI, runs production guard, builds DB
client via createDatabaseClient (not the Remix singleton), invokes
each query section, applies anonymization layer, writes JSON output.
- Production guard refuses NODE_ENV=production without
--i-know-what-im-doing, exactly like the demo seeder.
- Section runner wraps each query module in error handling so a single
failing query doesn't abort the run on a dry run — the data team
sees the full status picture (ok vs cohort_too_small vs
not_implemented) on every run.
- Defense-in-depth: strips rawValue fields before writing the output
JSON, even if a query forgets to drop them.
apps/webapp/scripts/state-of-em-2026/cli.ts (NEW)
- Hand-written argument parser, zero external CLI deps. Mirrors the
cli.ts shape used by seed-reporting-demo.
- Flags: --output, --csv, --data-window-start, --data-window-end,
--min-assets, --min-cohort-size, --internal-allowlist, --dry-run,
--i-know-what-im-doing, --help.
- Date parsing is strict YYYY-MM-DD; data-window-end > data-window-
start is enforced.
apps/webapp/scripts/state-of-em-2026/cohort.ts (NEW)
- Eligibility filter implementation. Builds the cohort per the
published methodology:
Organization.type = TEAM
AND Organization.workspaceDisabled = false
AND owner User.deletedAt IS NULL
AND NOT in internal allowlist
AND >= --min-assets assets at end of window
- Returns a CohortSummary with diagnostic counts (excluded by
asset-count, excluded by allowlist, final size, total assets)
that the orchestrator includes in the output JSON for transparency.
- Allowlist loader treats a missing file as empty + prints a warning.
Production runs must populate the file.
apps/webapp/scripts/state-of-em-2026/anonymize.ts (NEW)
- The two safety controls every aggregate passes through:
1. K-anonymity floor: aggregates with cohortSize < minCohortSize
are reported as null with status "cohort_too_small". The report
MDX treats these as unreportable.
2. One-significant-figure rounding (with optional override for the
rare stat where 1 sig fig collapses meaningful distinctions).
- `reportable()` is the wrapper every query function must call —
building a ReportableAggregate directly without it is the foot-gun
the reviewer should flag in code review.
- `stripRawValues()` is defense-in-depth: removes rawValue from
output even if a query module forgot to.
apps/webapp/scripts/state-of-em-2026/context.ts (NEW)
- Shared extractor context typed and threaded through every query.
Each query receives db + window dates + eligibleOrgIds + minCohortSize.
apps/webapp/scripts/state-of-em-2026/output-schema.ts (NEW)
- Output JSON shape. Mirrors the ReportStat/IndustryCut types in
website-v2's src/data/state-of-equipment-management-2026.ts so the
data team can paste values across without translation friction.
- buildEmptyOutput() helper for the orchestrator to start from.
apps/webapp/scripts/state-of-em-2026/README.md (NEW)
- Full workflow doc for the data team. Quick start, flag reference,
production-run options (clone vs read-replica vs direct), security
notes, how to add a new aggregate, the file tree.
apps/webapp/scripts/state-of-em-2026/methodology.md (NEW)
- Mirror of the report's published methodology section. Lives in this
repo so the script and the public report cannot drift out of sync.
- Includes inclusion criteria, anonymization rules, definitions
(active custody, ghost asset, idle asset, etc.), confidence levels,
reproducibility statement, limitations.
- Bump methodology version in lockstep with the report's frontmatter.
apps/webapp/scripts/state-of-em-2026/allowlist/internal-orgs.json (NEW)
- Empty placeholder. Production runs MUST populate this with Shelf
staff workspaces, demo workspaces, and support workspaces before
the script is run for the published report.
apps/webapp/package.json
- Added "report:state-of-em-2026" and "report:state-of-em-2026:staging"
scripts, mirroring the seed-reporting-demo convention.
package.json (root)
- Added "webapp:report:state-of-em-2026" and "...:staging" proxy scripts
so the data team can run from monorepo root, mirroring webapp:seed:*
convention.
Next commit lands the query module stubs under
apps/webapp/scripts/state-of-em-2026/queries/ — one file per finding
section, with documented Prisma query shapes and notImplementedAggregate
sentinels so the orchestrator runs end-to-end before any query is
implemented.
Second commit of the State of Equipment Management 2026 extraction kit. Foundation landed in d0b0f4f. This commit adds the seven query-module stubs the orchestrator calls, plus a barrel re-export. Each stub: - Has a top-of-file JSDoc listing every aggregate key it produces, with the stat label and (where helpful) the column-level Prisma query shape the data team should implement. - Returns notImplementedAggregate sentinels for every key, matching the stat keys defined in website-v2's src/data/state-of-equipment- management-2026.ts (and referenced from the report MDX). So when the data team plugs in real values, the keys already line up. - Documents cohort sub-filtering where the section needs it (e.g. bookings restricts to bookings-enabled workspaces; audits restricts to audits-enabled workspaces). The k-anonymity floor MUST be applied to the sub-cohort, not just the global eligible cohort. - Documents the precise definitions that distinguish this report from vendor-survey data — ghost asset, idle asset, active custody, conflict averted, etc. Definitions live in methodology.md; query comments cross-reference them. queries/visibility.ts - median_assets_per_workspace, median_users_per_workspace, pct_assets_with_active_custody, vis_assets_with_location, vis_assets_with_category, vis_assets_with_custom_fields, vis_median_fields_per_workspace, vis_top_categories. - Explicit note that vis_top_categories must restrict to categories with >= --min-cohort-size distinct workspaces represented, to avoid leaking a niche category. queries/bookings.ts - avg_bookings_per_workspace_per_month, bk_median_bookings_per_workspace_per_year, pct_bookings_with_conflict_averted, bk_median_lead_time_days, bk_pct_overdue, bk_median_overdue_hours, bk_peak_day. - Calls out the conflict-averted measurement question: this requires either an ActivityEvent for failed-create attempts or API-layer telemetry. If neither exists, the data team decides whether to add instrumentation or drop the stat. queries/custody.ts - cu_pct_assets_with_history, cu_median_handovers_per_asset_per_year, cu_top_handover_categories. - Reminder per the discovery: don't reconstruct history from Custody.updatedAt; use ActivityEvent rows with custody-related action enum values. queries/audits.ts - au_pct_workspaces_running_audits, au_pct_audited_assets_found, au_pct_audited_assets_missing, au_pct_audited_assets_unexpected, au_median_completion_days, median_audit_completion_days (alias). - Restricts to Organization.auditsEnabled = true sub-cohort. queries/disorder.ts - ds_ghost_asset_rate, ds_idle_asset_rate, ds_recovery_rate_found_via_scan, ds_median_recovery_days. - Ghost-asset definition is precise and matches the public report's methodology section verbatim. Window-function aggregate; likely cleaner as raw SQL than Prisma ORM. queries/industries.ts - Four industries: Education, IT & Technology, Media & Production, Construction & Field Operations. - Industry assignment via UserBusinessIntel.primaryUseCase / industry on the workspace owner record. Workspaces without business intel are bucketed as Unspecified and excluded from per-industry stats (the global eligible cohort still includes them). - Per-industry k-anonymity floor enforced independently. queries/top-performers.ts - Top-quartile definition: bottom quartile of missing rate AND top quartile of on-time return rate. - Four patterns measured as median-vs-rest deltas: early custody assignment, quarterly audit cadence, QR labels at intake, kit grouping. Each carries its own k-anonymity check. - Explicit note that these are CORRELATIONS, not causal claims — matches the careful language in the report copy. queries/index.ts - Barrel re-export so the orchestrator's import block stays tidy and the data team has one file to update when adding modules. After this commit, the script runs end-to-end on a dry-run against any database with at least min-cohort-size eligible workspaces. The output JSON shows status="not_implemented" for every aggregate. The data team implements queries one section at a time; the orchestrator's status summary tracks progress.
🩺 React DoctorNo diagnostics directory passed — scan may have failed. Check the workflow logs. |
…et $ queries
Pivot the v1 extraction kit per editorial review (Musk-mode critique).
The original scaffold implemented 30+ stat shapes across 7 query
modules; the editorial verdict was that the report should be organized
around 8 prioritized stats with a single viral headline (ghost-assets-
in-dollars). This commit trims the kit to match.
apps/webapp/scripts/state-of-em-2026.ts (orchestrator)
- Removed runBookingsQueries, runCustodyQueries, runIndustryQueries,
runTopPerformerQueries calls from the orchestrator. Imports commented
out with a "DEFERRED v1.1" note pointing to restoration instructions.
- Now runs three modules only: visibility, audits, disorder.
- Added a reminder at the end of a successful run: the 8th headline
stat (survey_hours_lost_per_month_median) is NOT produced by this
script — it comes from the external survey tool and is plugged into
the website data file manually.
- Bumped SCRIPT_VERSION to 0.2.0 to track the scope change.
- Header comment updated to "v1 trimmed — 8 stats, ghost-asset headline".
apps/webapp/scripts/state-of-em-2026/queries/disorder.ts
- This is now the most important file in the kit — it contains the
headline stat.
- Replaced the 4 old stat stubs (ds_idle_asset_rate, ds_recovery_rate_
found_via_scan, ds_median_recovery_days, plus the original ds_ghost
_asset_rate) with the 4 v1.1 stat stubs aligned to the website data
file:
1. ds_ghost_asset_dollar_value_median_workspace (THE HEADLINE)
2. ds_ghost_asset_rate
3. ds_idle_asset_dollar_value_median_workspace
4. ds_recovery_dollar_value_total
- Implementation guidance comments updated for each:
- Ghost asset detection: window function over AuditAsset, likely raw
SQL. Dollar value = sum Asset.valuation over identified ghosts
per workspace, then MEDIAN across workspaces (right-skewed
distribution; mean would be dominated by outliers).
- Asset.valuation coverage caveat must be disclosed per the
methodology — partial coverage; published figure is a conservative
lower bound.
- Found-via-Scan recovery: requires the anonymous-source flag on
Scan; verify it exists before publication, otherwise mark
not_implemented.
apps/webapp/scripts/state-of-em-2026/queries/visibility.ts
- Trimmed from 8 stat stubs to 1: pct_assets_with_active_custody.
- The 7 cut stats (median_assets_per_workspace, median_users_per_
workspace, vis_assets_with_location, vis_assets_with_category,
vis_assets_with_custom_fields, vis_median_fields_per_workspace,
vis_top_categories) were demographic noise / feature-adoption rates
per the editorial review.
- The remaining stat is the upstream-cause stat for ghost assets —
the accountability gap that produces them in the first place.
apps/webapp/scripts/state-of-em-2026/queries/audits.ts
- Trimmed from 6 stat stubs to 2: au_pct_workspaces_running_audits
and au_pct_audited_assets_missing.
- The 4 cut stats (au_pct_audited_assets_found, au_pct_audited_assets
_unexpected, au_median_completion_days, median_audit_completion_
days) were either demographic detail or duplicates.
- Implementation guidance kept tight on the two survivors. Note the
audits-enabled sub-cohort needs its own k-anonymity check.
apps/webapp/scripts/state-of-em-2026/anonymize.ts
- Added optional `priorYearValue?: number | null` field to
ReportableAggregate type and an optional priorYearValue parameter
to reportable(). 2026 leaves this undefined; the 2027 edition pulls
the 2026 published value into this field per stat to render
year-over-year trends in the report MDX. The infrastructure is in
place now so 2027 doesn't need restructuring.
apps/webapp/scripts/state-of-em-2026/output-schema.ts
- Light cleanup of the doc comment. Schema is unchanged — the trim
is at the orchestrator level. industries field remains in the
output but is always {} in v1 (industries deferred to 2027).
apps/webapp/scripts/state-of-em-2026/README.md
- Major rewrite. New "Scope (v1.1)" section explains the 8-stat
pivot and lists exactly which stats come from this script vs from
the external survey tool.
- "What was deferred to 2027" section explicitly names the 4 query
modules that remain in the repo but are not invoked.
- Engineering estimate updated: ~30 hours total (down from the
original 50–60 hour estimate when 7 modules needed implementation).
Disorder.ts is the bulk (~15h) because ghost-asset detection is
the most complex query.
- Implementation order: disorder → audits → visibility. Most
important first.
apps/webapp/scripts/state-of-em-2026/methodology.md
- Major rewrite to mirror the new public report methodology:
- Two data sources (telemetry + survey) instead of telemetry only.
- The Ghost-asset dollar value section is now prominent and
discloses the Asset.valuation coverage caveat explicitly
({{TODO: pct_assets_with_valuation}}% covered).
- Added "Idle-asset dollar value" and "Recovery via Found-via-
Scan" definitions.
- New "Survey methodology" section documenting the n=200 admin
survey design.
- The methodology version remains 1.0 — substance unchanged,
presentation tightened to match the trimmed report.
The query stubs for bookings, custody, industries, and top-performers
remain in apps/webapp/scripts/state-of-em-2026/queries/ unchanged
from the previous commit — they are deliberately preserved as the
historical record + restoration target for the 2027 edition. The
orchestrator does not call them.
Mirrors the website-v2 ed05810 commit. CEO risk-management review surfaced that the v1.1 ghost-asset headline depended on the paid Audits add-on. If audits adoption is below ~20% of Team workspaces, the "median workspace" framing was dishonest — it was really "median of an audit-enabled minority". v1.2 pivots the headline to idle-asset dollar value, computed from universal ActivityEvent telemetry that fires for every Shelf workspace regardless of feature mix. Ghost-asset stats remain in the report but are demoted to a properly-qualified "audit-enabled subset" finding. New: apps/webapp/scripts/state-of-em-2026/probe.ts - Feature-adoption probe that runs FIRST. Measures audits enabled, audits run in window, bookings activity, Asset.valuation coverage, and the anonymous-scan capability check (Scan.userId IS NULL signal). - Each measurement compared against a published threshold mirrored from the website data file's reportMetadata.adoptionThresholds block. - Emits per-stat recommendation: publish / qualify / convert / drop. The human reviewer reads the probe before any query is implemented. - Probe output is internal-only — never published, never committed. apps/webapp/scripts/state-of-em-2026.ts (orchestrator) - Added --probe dispatch that runs only the probe and writes probe.json. - Un-deferred runBookingsQueries for the bk_pct_returned_late stat. - Bumped methodology version to v1.2 + script version to 0.3.0. - Header reflects the v1.2 idle-asset scope. apps/webapp/scripts/state-of-em-2026/cli.ts - Added --probe flag, ExtractorCliOptions.probe field, USAGE update. - Examples reordered: probe is step 1, dry-run is step 2, real run is 3. apps/webapp/scripts/state-of-em-2026/queries/disorder.ts - Idle-asset stats become primary. Headline is now ds_idle_asset_dollar_value_median_workspace; ds_idle_asset_rate is the percentage companion. - Recovery stat kept (ds_recovery_dollar_value_total) with note that the probe gates it on anonymous-scan capability. - Ghost-asset rate kept but with explicit "audit-enabled subset only" framing in the label. au_pct_audited_assets_missing stays in audits.ts. - Idle definition matches the website MDX three-clause definition: no ActivityEvent in prior 90 days AND created before window opened. apps/webapp/scripts/state-of-em-2026/queries/bookings.ts - Un-deferred — emits only bk_pct_returned_late (the late-return stat). - Implementation sketch uses BOOKING_CHECKED_IN ActivityEvent timestamp because Booking model has no actualReturnAt column. - Sub-cohort = workspaces with non-DRAFT bookings in window. apps/webapp/scripts/state-of-em-2026/methodology.md - New "Risk disclosures" section: feature-adoption risk with explicit thresholds table (auditsEnabled 5%, auditsRun 3%, bookingsActive 10%, valuationCoverage 30%), Asset.valuation coverage, cohort-size enforcement, survey response rate. This is the trust-signal that earns the citation. - "Idle asset" gets the precise three-clause definition. - "Late return" gets a precise definition matching bookings.ts. - "Ghost asset" relabeled as the audit-enabled subset definition. - Methodology version bumped to 1.2. apps/webapp/scripts/state-of-em-2026/README.md - Scope section explains both pivots (v1.0→v1.1 and v1.1→v1.2). - Eight-stat table updated with the new ordering. - New "Why --probe runs first" section explaining the risk discipline. - Workflow updated with probe as step 1. - Implementation order updated: disorder is still bulk of the work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
V1 scaffold for the data-extraction script that produces the anonymized aggregates feeding the public State of Equipment Management 2026 report on
shelf.nu/reports/state-of-equipment-management-2026.This is the data-team half of a two-repo effort. Companion PR:
shelf-nu/website-v2#142— that PR scaffolds the public report page, the typed data structure this script's output JSON feeds into, and the distribution materials (press release, share copy, Wikipedia citation guide).Both PRs are draft. Both should stay draft until the data team implements the query modules, the script has been run against a real cohort, the output JSON values have been copied into the website data file, and editorial has signed off.
Why this matters
Industry reports are the single highest-multiplier content asset Shelf can ship. The compounding mechanism is original first-party data nobody else has — vendor surveys don't compound, but telemetry-backed numbers with open methodology do. Wikipedia editors, journalists, analysts, and AI training pipelines all reference this kind of report; a well-cited stat from the 2026 edition will keep appearing in LLM responses for years.
The whole thing only works if:
This PR scaffolds the pipeline that ensures all three.
What's in this PR
2 commits, 14 files under
claude/state-of-equipment-management-2026:d0b0f4f0b8aab6queries/, one per finding section (visibility, bookings, custody, audits, cost-of-disorder, industries, top-performers), plus a barrel re-exportFile tree
Decisions made (with safety reasoning)
Mirror the
seed-reporting-demo.tspattern exactly. Same production guard (NODE_ENV=productionrequires--i-know-what-im-doing), samecreateDatabaseClient()(not the Remix singleton — see comment in seed file), same dotenv invocation, same root-package proxy script naming. Reviewers familiar with the demo seeder will read this script the same way.K-anonymity floor of 20 workspaces baked into the anonymization layer. Every aggregate's
cohortSizeis checked against--min-cohort-size(default 20); under-cohort aggregates are reported asnullwithstatus: "cohort_too_small". The report MDX treats those as unreportable. This is non-optional and defense-in-depth — every query function callsreportable(...)to construct results; building aReportableAggregatedirectly bypasses the check and reviewers should flag it.One-significant-figure rounding by default. Aggregates land in the output JSON pre-rounded, so even if a downstream consumer publishes the JSON value directly, they can't leak precision. The
rawValuefield is stripped before write as defense-in-depth.Internal allowlist is a separate file, not hard-coded in source. The placeholder at
allowlist/internal-orgs.jsonis empty; production runs must populate it with Shelf staff workspaces, demo workspaces, and support workspaces before publication. The cohort builder prints a warning if the file is empty.not_implementedsentinel, not0. Stub queries returnnotImplementedAggregatewith explicit status, so a dry-run run shows clearly which sections are stubs vs implemented. The orchestrator's status summary tabulates ok / cohort_too_small / not_implemented counts and warns if any not_implemented remain.Read-only operation, no writes. The script never inserts or updates rows. It only reads from the eligible cohort. (The cohort builder uses Prisma's
findMany+ JS-side filtering rather than a raw query that could be mistaken for write-capable code.)Section failures don't abort the run.
runSection()wraps each query module in try/catch — a single failing query during dry-run yields a logged error for that section and the orchestrator continues. This lets the data team see the full status picture early; they can fix one section at a time.CSV companion path is parsed but not yet implemented.
--csv <path>is accepted and prints a "not yet implemented" warning when used. Stub keeps the orchestrator interface stable for when the data team adds CSV emission.No read-replica wiring. The discovery confirmed there's no
DATABASE_URL_REPLICAenv var in the codebase today. The README documents three options for production runs (clone, add a replica env var, or run direct with the guard flag) and recommends the data team add a replica env var as part of their implementation work. The script'screateDatabaseClient()call accepts an optional URL — a one-line change insidestate-of-em-2026.tsadds replica support when ready.Methodology lives in both repos. Mirror copy at
apps/webapp/scripts/state-of-em-2026/methodology.mdand at the published report's#methodologysection. The README and source code both say: change both in the same commit.What v1 does NOT include
notImplementedAggregatesentinels. The data team writes the Prisma / raw-SQL queries one section at a time.Workflow for the data team
Production run guidance
The script is intended to be run against the production database because it needs the real customer data to produce meaningful aggregates. The README documents three options listed safest to most convenient:
DATABASE_URL_REPLICAenv var + one-line change instate-of-em-2026.tsto use it when present.--i-know-what-im-doing. Read-only, paginated, off-peak hours.The production guard refuses to run with
NODE_ENV=productionunless--i-know-what-im-doingis passed.Security review notes
This script does cross-organization aggregation, which is unusual in the Shelf codebase. The lefthook Claude security review and the human reviewer should examine:
Organization.name,User.email,Asset.title, etc. to output. Only counts, medians, and percentages.stripRawValues. The orchestrator stripsrawValuefrom every aggregate before writing the output. Even if a query forgets to drop them, the orchestrator does.Test plan
pnpm webapp:typecheckpasses (the script and its modules are TypeScript-strict)pnpm webapp:lintpassesnot_implementedstatus (expected for v1)Companion PR
The website-v2 PR (#142) holds the report page, the typed data structure values land in, and the distribution materials. Both PRs ship together.
Generated by Claude Code