CapEx feeds & findings
The Atlas CapEx engine is the capital-governance data-quality engine behind these pages:
- Capex Monitor
- Capex Lineage
- the Analyst and CFO dashboards
- Progress
- the DQ overview’s CapEx score
CORE ported the engine from Atlas and none of its numbers. An operator uploads three feeds, the console stores them in its own appfs root, and every figure on every page is computed from what was uploaded.
The contract is CapexService in grpc/operators/core/api/proto/runink/core/atlas/v1/capex.proto. The engine is the stdlib package grpc/operators/core/internal/atlas/capex. The store is grpc/operators/core/internal/console/atlas_capex_store.go.
The three feeds
Each feed is a CSV with a fixed header contract (internal/atlas/capex/parse.go, feedSpecs). The contract is also returned in GetFeeds, so the upload dialog can show it without hard-coding it.
Feed (Feed enum) | Columns, in order | Must not be blank |
|---|---|---|
FEED_CAPITAL_PLAN (capital_plan) | ItemId, Description, Type, Category, Lead_time_in_weeks, PurchaseDate, Units, PricePerUnit, Amount | Description, Amount |
FEED_CAPITAL_REQUEST (capital_request) | ID, RequestDate, ItemId, ApprovalDate, ApprovalId, Vendor, Amount | ID, Amount |
FEED_SPEND (spend) | PoNo, PODate, Amount, ApprovalId, ItemId, Type | PoNo, Amount |
Header names are trimmed. The first column with a given name wins. Dates must be ISO YYYY-MM-DD. A date that does not parse raises its own finding (CP-008, CR-010, SP-006).
Ingest: stage → preview → commit
Ingest replaces whole feeds. It has three steps, ported from Atlas’s ImportFeedsDialog. There is no row-level edit and no merge, so the uploaded file is the feed.
StageFeed
Parses one CSV for one feed and holds the result server-side under a stage_id. The current feeds do not change. A file-level problem is returned as a ParseIssue in errors with an empty stage_id, not as a gRPC error, because the file is at fault rather than the caller.
PreviewIngest
Evaluates the rules over the candidate dataset: the staged feeds overlaid on the current ones. It returns the before/after summary and the candidate findings. It is a read, so it writes nothing and records no scan run.
CommitIngest
Each staged feed replaces its current feed wholesale, and one ScanRun is recorded. The stages are consumed. An ingest takes at most one upload per feed.
ClearFeed empties one feed and records a ScanRun with trigger SCAN_TRIGGER_CLEAR. CORE never refills a feed with sample rows. There is deliberately no “reset to sample” RPC.
Caps (each one refuses; nothing is truncated)
| Cap | Value | What you get |
|---|---|---|
| CSV size per upload | 8 MiB | ParseIssue code too_large, nothing staged |
| Data rows per feed | 50,000 | ParseIssue code too_large, nothing staged |
| Encoding | UTF-8 only | ParseIssue code not_utf8, with the line of the first bad byte |
| Pending stages | 12 | RESOURCE_EXHAUSTED, naming when the oldest expires |
| Stage lifetime | 30 minutes | an expired stage_id is INVALID_ARGUMENT |
| Commit note | 1,000 bytes | INVALID_ARGUMENT |
| Scan history | 50 runs | the oldest is dropped |
| Unpaged per-row lists in a read | 500 entries | the read says so in provenance.unmeasured |
The gRPC-web transport accepts messages up to 16 MiB (atlasMaxRecvBytes). This keeps the 8 MiB feed cap, not the transport, as the limit a user meets.
Pulling a feed from a connection
PullFeed stages a feed from a registered connection instead of an uploaded file:
- It runs one bounded, read-only
SELECTof the mapped columns from one dataset. Identifiers are validated and engine-quoted. LIMITis the row cap + 1, so “exactly at the cap” and “over the cap” can be told apart.- It has a 90-second budget (
capexPullBudget). - It uses the same
store/estateruntime and the same audited, one-call credential read as Resolve.
The rows are written as CSV and handed to StageFeed unchanged. A pulled stage therefore gets every upload check, and a person still commits it after the preview.
Unlike Resolve, a pull keeps cells. A feed is data by definition, just as an uploaded CSV is.
The request has an optional runner field. It follows the same rules as a Resolve action: absent means the connection’s binding, console means Runink managed, and a name means that runner. See Resolve.
A commit whose every stage was pulled records its ScanRun as SCAN_TRIGGER_CONNECTION, and each feed’s filename reads <connection> · <dataset>. If even one uploaded file is in the commit, the trigger is SCAN_TRIGGER_UPLOAD (capexCommitTrigger).
Scan runs
ScanTrigger | Cause |
|---|---|
SCAN_TRIGGER_UPLOAD | A CommitIngest that includes at least one uploaded CSV |
SCAN_TRIGGER_CONNECTION | A CommitIngest whose every stage was pulled |
SCAN_TRIGGER_MANUAL | A playbook’s capex_scan step. The proto marks a hand-requested re-scan as reserved for a later phase |
SCAN_TRIGGER_CLEAR | A ClearFeed |
ListScanRuns is the Progress page’s history, newest first.
Findings are recomputed on read and never stored. They are cached by upload tables, Go-Live date and governance scope. This is why a ListFindings page_token fails with an INVALID_ARGUMENT like the following once anything changes between pages:
the findings changed (a new ingest, Go-Live date, rule status or threshold) since this page_token was issued — list again from the first page
Configuration: the Go-Live date
GetConfig returns the engine configuration. Today the only configurable field is go_live_date. With nothing stored, it answers Atlas’s default, 2027-03-31, with provenance.source = DATA_SOURCE_DEFAULT, so the page can say the date is a default rather than a decision.
UpdateConfig changes the date. That changes CP-007’s verdicts on the next read. It does not record a ScanRun, because no feed changed.
The other engine constants are changed only through rule governance, as PARAMETER changes. Their defaults (capex.DefaultConfig) are:
- 14-day schedule tolerance
- $500 capital unit-price threshold
- 3-day minimum approval lag
- 5% request/plan tolerance
- 1.10 overspend factor
The rule book
GetRuleBook returns every rule: Atlas’s 21, in Atlas’s order, followed by three date rules that CORE added (internal/atlas/capex/rules.go).
| Rule | Sheet | Severity | Condition |
|---|---|---|---|
| CP-001 | capital_plan | Critical | ItemId cannot be blank |
| CP-002 | capital_plan | Critical | Every item must have a valid ItemId (unique, well-formed) |
| CP-003 | capital_plan | Medium | Description must be meaningful — not generic like ‘Tool’ |
| CP-004 | capital_plan | High | Type must be Manufacturing, Tools, or Testing |
| CP-005 | capital_plan | High | Category must be Capital or Non-Capital |
| CP-006 | capital_plan | Critical | Unit price < $500 must be Non-Capital |
| CP-007 | capital_plan | Critical | PurchaseDate must align with Go-Live − Lead_time_in_weeks (±14d tolerance) |
| CR-001 | capital_request | Critical | ItemId cannot be blank |
| CR-002 | capital_request | Critical | ItemId must reference capital_plan |
| CR-003 | capital_request | Critical | ApprovalId cannot be blank |
| CR-004 | capital_request | High | Vendor cannot be blank |
| CR-005 | capital_request | Critical | RequestDate must not exceed plan PurchaseDate |
| CR-006 | capital_request | Medium | RequestDate aligned with plan PurchaseDate (±14d) |
| CR-007 | capital_request | High | ApprovalDate ≥ RequestDate + 3 days |
| CR-008 | capital_plan | Critical | Every capital_plan item must have at least one capital_request |
| CR-009 | capital_request | High | Request amount must be within 5% of the capital_plan amount |
| SP-001 | spend | Critical | ApprovalId, ItemId, and Type cannot be blank |
| SP-002 | spend | Critical | PODate must be ≥ ApprovalDate (capital_request) |
| SP-003 | spend | Critical | Spend > approved amount × 1.10 requires additional approval |
| SP-004 | spend | Critical | ApprovalId must reference capital_request |
| SP-005 | spend | High | Spend Type must match capital_plan Type for the same ItemId |
| CP-008 | capital_plan | Critical | PurchaseDate must be a valid ISO date (YYYY-MM-DD) |
| CR-010 | capital_request | Critical | RequestDate and ApprovalDate must be valid ISO dates (YYYY-MM-DD) |
| SP-006 | spend | Critical | PODate must be a valid ISO date (YYYY-MM-DD) |
CR-008 is a capital_request rule reported on the capital_plan sheet, because the defect is the plan row that has no request. That is why Sheet and Feed are separate enums.
Control-effectiveness dimensions
The dashboards group rules into Atlas’s dimensions (dashboard.go):
| Dimension | Rules |
|---|---|
| Completeness | CP-001, CR-003, CR-005, SP-001 |
| Validity | CP-004, CR-004, SP-005 |
| Consistency | CR-002, SP-002, SP-004 |
| Accuracy | CP-005, CR-006, SP-003 |
| Timeliness | No rule mapped. Absent |
| Uniqueness | No rule mapped. Absent |
Rule status changes what is scored
Only APPROVED rules are scored:
- PENDING and DRAFT rules still produce findings, flagged
dry_run, but those findings are excluded from every figure: summary, dashboards, insights and scan runs. Each excluded rule is named inprovenance.unmeasured. - REJECTED rules produce no findings at all.
Approved thresholds feed the engine configuration. See Rule governance.
Dashboards and absence
GetAnalystDashboard and GetCfoDashboard are computed from the current feeds. A section the feeds cannot answer is listed in absent_sections, with the reason, and is never filled. On the CFO page, for example, forecast variance, capital efficiency, ROI and the executive summary are absent.
Who may write
Every CapEx write is admin-write under CORE_ATLAS_ADMINS, and each is audited under resource atlas-capex:
| RPC | Audit action |
|---|---|
StageFeed | atlas.capex.stage |
PullFeed | atlas.capex.pull |
CommitIngest | atlas.capex.commit |
ClearFeed | atlas.capex.clear |
UpdateConfig | atlas.capex.config.update |
On a console with no sign-in configured, each write is refused with a message that begins this console has no sign-in configured, so an Atlas CapEx change would be unattributable.