Skip to content

[v1 scaffold · DRAFT] State of Equipment Management 2026 — anonymized aggregate extraction kit - #2570

Draft
carlosvirreira wants to merge 4 commits into
mainfrom
claude/state-of-equipment-management-2026
Draft

[v1 scaffold · DRAFT] State of Equipment Management 2026 — anonymized aggregate extraction kit#2570
carlosvirreira wants to merge 4 commits into
mainfrom
claude/state-of-equipment-management-2026

Conversation

@carlosvirreira

Copy link
Copy Markdown
Contributor

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:

  1. The numbers are real.
  2. The methodology holds up under scrutiny.
  3. The aggregation pipeline cannot leak customer data.

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:

Commit What
d0b0f4f Foundation — orchestrator entry point, CLI parser, eligibility cohort builder, anonymization layer, output schema, README, methodology mirror, package.json wiring
0b8aab6 Query module stubs — seven files under queries/, one per finding section (visibility, bookings, custody, audits, cost-of-disorder, industries, top-performers), plus a barrel re-export

File tree

apps/webapp/
  scripts/
    state-of-em-2026.ts                   # NEW — orchestrator entry
    state-of-em-2026/
      README.md                           # NEW — full workflow doc
      methodology.md                      # NEW — mirrors public report methodology
      cli.ts                              # NEW — argument parser
      context.ts                          # NEW — shared extractor context
      cohort.ts                           # NEW — eligibility filter (implemented)
      anonymize.ts                        # NEW — k-anonymity + sig-fig rounding (implemented)
      output-schema.ts                    # NEW — typed output JSON shape
      allowlist/
        internal-orgs.json                # NEW — empty placeholder; data team populates
      queries/
        index.ts                          # NEW — barrel re-export
        visibility.ts                     # NEW — Finding 1 stubs
        bookings.ts                       # NEW — Finding 2 stubs
        custody.ts                        # NEW — Finding 3 stubs
        audits.ts                         # NEW — Finding 4 stubs
        disorder.ts                       # NEW — Finding 5 stubs
        industries.ts                     # NEW — per-industry stubs
        top-performers.ts                 # NEW — pattern stubs
  package.json                            # +report:state-of-em-2026 + ...:staging scripts

package.json                              # +webapp:report:state-of-em-2026 proxy scripts

Decisions made (with safety reasoning)

  1. Mirror the seed-reporting-demo.ts pattern exactly. Same production guard (NODE_ENV=production requires --i-know-what-im-doing), same createDatabaseClient() (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.

  2. K-anonymity floor of 20 workspaces baked into the anonymization layer. Every aggregate's cohortSize is checked against --min-cohort-size (default 20); under-cohort aggregates are reported as null with status: "cohort_too_small". The report MDX treats those as unreportable. This is non-optional and defense-in-depth — every query function calls reportable(...) to construct results; building a ReportableAggregate directly bypasses the check and reviewers should flag it.

  3. 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 rawValue field is stripped before write as defense-in-depth.

  4. Internal allowlist is a separate file, not hard-coded in source. The placeholder at allowlist/internal-orgs.json is 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.

  5. not_implemented sentinel, not 0. Stub queries return notImplementedAggregate with 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.

  6. 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.)

  7. 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.

  8. 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.

  9. No read-replica wiring. The discovery confirmed there's no DATABASE_URL_REPLICA env 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's createDatabaseClient() call accepts an optional URL — a one-line change inside state-of-em-2026.ts adds replica support when ready.

  10. Methodology lives in both repos. Mirror copy at apps/webapp/scripts/state-of-em-2026/methodology.md and at the published report's #methodology section. The README and source code both say: change both in the same commit.

What v1 does NOT include

  • No real query implementations. Every query function returns notImplementedAggregate sentinels. The data team writes the Prisma / raw-SQL queries one section at a time.
  • No populated internal allowlist. The data team's first task is identifying Shelf staff / demo workspace IDs and adding them.
  • No CSV emission. Flag accepted, implementation deferred — see decision Basics setup #8.
  • No read-replica env var. Recommended for production runs but not added in this PR — see decision basic edit view of user #9.
  • No tests. The cohort filter and anonymization helpers are testable in isolation; adding unit tests is straightforward but not in this v1.

Workflow for the data team

   1. Populate `allowlist/internal-orgs.json` with Shelf staff workspace IDs.
   2. Run the script in dry-run mode first to verify cohort size:
        pnpm webapp:report:state-of-em-2026 -- --dry-run
   3. Implement query modules under `queries/` one section at a time.
      Re-run dry-run after each to confirm the section's stats flip
      from `not_implemented` to `ok`.
   4. When all sections are `ok`, run the real extraction:
        pnpm webapp:report:state-of-em-2026 -- --output ./output/aggregates.json
   5. Copy the values from `aggregates.json` into the website-v2 PR's
      `src/data/state-of-equipment-management-2026.ts` data file.
   6. Optionally: implement CSV emission and produce
      `state-of-equipment-management-2026.csv` for publication alongside
      the report.

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:

  1. Run against a fresh staging clone of production data. Zero load on production.
  2. Add DATABASE_URL_REPLICA env var + one-line change in state-of-em-2026.ts to use it when present.
  3. Direct against production with --i-know-what-im-doing. Read-only, paginated, off-peak hours.

The production guard refuses to run with NODE_ENV=production unless --i-know-what-im-doing is 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:

  • No customer data ever leaves an aggregate. Confirmed: nothing in the codebase writes Organization.name, User.email, Asset.title, etc. to output. Only counts, medians, and percentages.
  • Sub-cohort k-anonymity. Every query that subsets the global cohort (e.g. "audits-enabled workspaces only") must apply the k-anonymity floor to the sub-cohort, not just rely on the global floor. The query stubs note this; reviewers should verify each implementation does so.
  • Defense-in-depth stripRawValues. The orchestrator strips rawValue from every aggregate before writing the output. Even if a query forgets to drop them, the orchestrator does.
  • Multi-tenancy is strict in this codebase. This script is intentionally the exception; reviewers may want to verify it lives clearly outside the request-handling path and uses its own DB connection.

Test plan

  • pnpm webapp:typecheck passes (the script and its modules are TypeScript-strict)
  • pnpm webapp:lint passes
  • Dry run against a staging DB:
    pnpm webapp:report:state-of-em-2026:staging -- --dry-run
    
    • Confirms CLI parsing works
    • Cohort builder reports a sensible eligible count
    • All 30+ aggregates report not_implemented status (expected for v1)
    • Status summary prints a warning that not_implemented count is > 0
  • Try invalid CLI arguments — confirm clear error messages:
    pnpm webapp:report:state-of-em-2026 -- --data-window-start invalid-date
    pnpm webapp:report:state-of-em-2026 -- --min-assets 0
    pnpm webapp:report:state-of-em-2026 -- --unknown-flag
    
  • Try production guard:
    NODE_ENV=production pnpm webapp:report:state-of-em-2026:staging -- --dry-run
    # should exit 2 with refusal message
    NODE_ENV=production pnpm webapp:report:state-of-em-2026:staging -- --dry-run --i-know-what-im-doing
    # should run
    

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

… 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.
@github-actions

Copy link
Copy Markdown

🩺 React Doctor

No 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant