metricfile

package
v0.33.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package metricfile parses the committed file that defines one metric in a pipeline folder, and fingerprints what a push of it would send.

STDLIB ONLY, exactly as internal/tabledocs is, and for the same reason: this is the one place that decides what a metric file MEANS, and a leaf with no Ronja imports can be read and tested without any of the loop around it.

THE ONE RULE WORTH STATING UP FRONT: the recipe grammar belongs to the SERVER, and nothing here may become a second copy of it. `rdb.ValidateMetricRecipe` decides what a recipe may say — which aggregates exist, what a `value` expression may combine, what a native grain is — and a Go shape for it here would be a second thing to keep in step, whose drifted version would accept a recipe the compiler then refused (or, worse, silently DROP a key somebody wrote). So the recipe is carried as json.RawMessage from the file straight to the wire, and this package reaches into exactly one field of it: `source`, which has to be rewritten from the folder's alias to this organization's id before the request. Every other key — including one this build has never heard of — is passed through verbatim BY THIS PACKAGE.

⚠️ AND IS THEN DROPPED BY THE SERVER, TODAY, IN SILENCE. That last sentence is a statement about this file and nothing further: `rdb.DecodeMetricRecipe` is a plain json.Unmarshal with no DisallowUnknownFields and the canonical form stored in `metric_recipe` is a re-marshal of the TYPED struct, so a key the server's grammar does not know reaches it, is ignored, and is absent from what is persisted — with no warning on any write path. The file keeps the key, the row never had it, and nothing compares the two. Do not read "passed through verbatim" as a guarantee that it ARRIVES: closing that is BL-8c71 (claude/backlog/), which owns the decision between refusing an unknown key and warning about one, and owns bringing this header into line with whichever it picks.

The top level is deliberately the OPPOSITE rule, and it is one this package can actually keep. Unknown keys at the TOP level are an error, on the docs sidecar's rule (cli/README.md: "a dropped key is the failure this whole loop exists to remove") — a `verified` or an `owner` somebody adds expecting it to be honoured must be refused where it was written, not ignored on every push for ever. Unknown keys INSIDE the recipe are not refused here, because there the grammar is not this package's to know.

Index

Constants

View Source
const (
	MaxTagNameBytes  = 64
	MaxTagsPerMetric = 20
)

MaxTagNameBytes and MaxTagsPerMetric are the server's two tag caps (rtag.MaxTagNameLen, rtag.MaxTagsPerResource), mirrored so a file that breaks either is refused where it was written rather than at push time, half way through a folder.

⚠️ BYTES, NOT CHARACTERS. The server measures a name with len(), so 33 `ö`s — 33 characters — are 66 bytes and refused. A client counting runes would pass exactly the names the server then refuses.

⚠️ MaxTagsPerMetric bounds the FILE, and only the file: the server's cap is on the metric's FINAL set, which also holds whatever colleagues added in the web app. A file within it can still be refused there, which the push reports and records rather than predicting here.

Variables

This section is empty.

Functions

func Fingerprint

func Fingerprint(resolvedRecipe json.RawMessage, description, reportingTimezone *string) string

Fingerprint is this file's answer to "has the local file changed since the last push from this folder" — taken over the WHOLE of what a push would send, which is the resolved recipe plus both three-state fields.

A SECOND fingerprint beside the lock's liveSHA256, and the two are not interchangeable, by the invariant wfdir.LockTable states: this one is taken from the FILE and compared against the file, liveSHA256 is taken from the ROW and compared against the row. Comparing the file against the row cannot work here at all — the row holds the recipe the server CANONICALIZED (its version defaulted, its source a real id), and the file holds aliases and may omit a defaultable key, so a byte comparison would report every metric as pending for ever and re-push it on every run.

The resolved recipe is the argument rather than being computed here because resolution needs the folder's alias codec, which a stdlib-only leaf has no business holding. The three parts are separated by a NUL, so a description ending in the timezone's first characters cannot alias with the timezone itself, and each three-state field distinguishes ABSENT from an empty claim — they are different instructions, and a fingerprint that folded them would miss the edit that clears a description.

func FoldTagName added in v0.32.2

func FoldTagName(name string) string

FoldTagName is the server's tag-name equality, lower(trim()), in Go — the ONE comparison the metric loop's tag half makes, in the parser, the pending predicate and the lock alike.

It can disagree with Postgres' lower() on a few non-ASCII letters (İ, ß); the server's handler folds the same way before its merge, so a name the two disagree on is at worst reported pending once and then matched by id.

func RecipeSHA256

func RecipeSHA256(recipe json.RawMessage) string

RecipeSHA256 fingerprints a recipe the way the SERVER's precondition is expressed: sha256 over the raw bytes, lowercase hex.

⚠️ THE BYTES MUST BE THE ONES THE SERVER SENT, HASHED EXACTLY AS RECEIVED. This is the digest `baseRecipeSha256` compares against, and the server takes it over the recipe as this API SENDS it — the `recipe` in the recipe route's own response, or `metricRecipe` from GET /feature/model/<draft>, which are the same document. So a client that decoded the recipe into a map and re-marshalled it would hash a different key order and the compare-and-swap would fail against an edit nobody made. That is why the recipe is carried as json.RawMessage from the response all the way here, and why Resolve's re-assembled form is explicitly NOT what this is called on.

NOT the bytes STORED in `metric_recipe`, which is what this said before and what the server briefly did. `metric_recipe` is JSONB, so Postgres re-serializes what was written (keys sorted length-then-bytes, a space after `:` and `,`) — and Go's encoder COMPACTS an embedded json.RawMessage on the way out, stripping exactly those spaces from every surface that publishes it. No caller could ever hold the stored form, so a precondition taken over it was satisfiable by NOBODY: every conditional push 409'd against an edit nobody made. It was invisible from this side, because the CLI's fake instance computes both ends of the comparison the same way and so cannot disagree with itself. The server now hashes the wire form (feature.recipeSHA256 → canonicalRecipeBytes) and returns the same bytes from both surfaces, which makes "hash what you were handed" — always the right advice — true as well.

An absent recipe (a row that is not a metric, or a JSON `null`) hashes to nothing rather than to the digest of the four bytes "null": empty is what every guard in this loop reads as "no answer", and a real digest there would arm a comparison against a recipe that does not exist.

func TagsFingerprint added in v0.32.2

func TagsFingerprint(tags []string) string

TagsFingerprint is the fingerprint of a file's tag list that a recorded tag REFUSAL is held against: while the file still says the same thing, the refusal stands and the metric is not tag-pending, so a folder whose tags the server refused does not re-send the same doomed request on every push.

Over the FOLDED, SORTED set rather than the file's bytes: reordering or recasing the list changes nothing the server would answer, so it must not read as an edit worth retrying. NUL-separated, so no two sets collide.

Types

type File

type File struct {
	// Recipe is the definition EXACTLY as the file spells it — aliases and all,
	// unknown keys and all. Never sent as it stands: Resolve rewrites `source`
	// first.
	Recipe json.RawMessage
	// Description is the metric's prose, three-state (see metricFile).
	Description *string
	// ReportingTimezone is the calendar the metric is evaluated at, three-state.
	// An explicit "" is a RESET to the UTC literal, which the recipe route reads
	// as its own third state; an absent key leaves the declaration alone.
	ReportingTimezone *string
	// Tags is the list of tag names the folder ASSERTS on the metric,
	// three-state: nil is a folder that does not manage the metric's tags at
	// all, an empty list asserts none, and names are trimmed and otherwise kept
	// as spelled (case is the server's to fold).
	//
	// ⚠️ NOT PART OF Fingerprint, deliberately. Tags are not drafted — they sit
	// on the live metric and a tag write needs no checkout, no build and no
	// publish — so folding them into the recipe fingerprint would turn every tag
	// edit into a re-staged, re-built draft of an unchanged definition. The tag
	// half has its own record and its own pending question; see TagsFingerprint.
	Tags *[]string
}

File is one parsed metric file.

func Parse

func Parse(body []byte) (File, error)

Parse decodes one metric file.

Unknown TOP-LEVEL keys are an ERROR rather than being dropped, exactly as in the docs sidecar and the `ronja automation` loop's curated JSON: a dropped key is the failure the whole sync idea exists to remove — the file says one thing, the row keeps another, and nothing anywhere reports a difference.

func (File) Resolve

func (f File) Resolve(sourceID string) (json.RawMessage, error)

Resolve returns the recipe with `source` replaced by the id given, which is the form that goes over the wire.

EVERY OTHER KEY IS CARRIED VERBATIM, including one this build does not know about: the top-level object is decoded into raw fields and re-assembled, so nested values are the file's own bytes and only `source` is rewritten. Key ORDER is not preserved (Go marshals a map in sorted order) and does not need to be — this is the form that is SENT, and the form that is HASHED for comparison against a previous send, never the form the server's own compare-and-swap digest is taken over. That digest is always taken from bytes the server itself produced; see RecipeSHA256.

func (File) Source

func (f File) Source() (string, error)

Source reads the name the recipe's `source` names — an alias this folder declares, or a literal table id.

It is the ONE field this package looks inside the recipe for, because it is the one that cannot survive the trip: a folder deployed to two organizations spells its source as a name, and each organization's server only knows an id. A missing or non-string `source` is refused HERE rather than left to the server, because the refusal a caller can act on names the FILE.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL