cube(2): CUBE_OP_FLAG_SCAN — classify at write, retrieve by class

The store's flag field is a substrate, and this is its read half: a v4
index entry carries a 16-bit class mask, and a scan by class is a walk that
reads the mask and tests it. Nothing about the merge changes — the same log
overlay, the same order — so a caller pays for its own class rather than for
the store.

The op takes a space, a mask, a mode (any/all), a cursor and a buffer, in
its own size-versioned block. A mask of zero matches nothing, because naming
no class is asking no question. Records come back as
key | flags(2) | value_len | value: the mask travels, since a record can
carry bits the scan did not name and no other op returns a mask.

Both layouts the mask can be in are read. With the record still in the log
it comes from the v2 log entry; after a fold it comes from the v4 index
entry. The non-indexed path is not a corner — it is the state of every store
between the write that classified something and the fold, so answering it
with 'nothing' would make the substrate work only after a checkpoint.

Also settles what an append writes into which log, since the entry's frame
has to match the header a reader frames it by: a log that already holds v1
entries keeps taking v1 entries (a device folds first — that is the v4
migration — and a bare image keeps the log it has), an empty log is framed
v2 on a device and left alone on a bare image, and a log with no header is
framed v2 on a device and v1 on a bare image. The bare layout is the legacy
one: it has no index for a mask to be folded into, so a mask written there
could only be scanned and never checkpointed — half a feature, bought by
making every existing reader of that layout grow a version it cannot use.
This commit is contained in:
surface-camera-build
2026-09-22 00:08:19 -04:00
parent 0267831b18
commit 41e437c07a
3 changed files with 549 additions and 46 deletions
+46
View File
@@ -43,6 +43,7 @@ struct cube_args {
#define CUBE_OP_ENUM 5 /* walk the records of a space, in batches */
#define CUBE_OP_SPACES 6 /* walk the spaces that hold records */
#define CUBE_OP_RANGE 7 /* walk the records of a space that lie in a box */
#define CUBE_OP_FLAG_SCAN 8 /* walk the records of a space whose class mask matches */
/*
* The walk's argument block: its own block rather than a wider `cube_args`, because it needs a
@@ -111,4 +112,49 @@ struct cube_range_args {
*/
};
/*
* The flag scan's argument block: `CUBE_OP_FLAG_SCAN`, the class-mask half of the store's
* classification substrate (DESIGN-flag-vocabularies.md). Its own block for the walks' reason it
* needs a cursor and a buffer and versioned by `size` like the other three.
*
* `mask` is a raw 16-bit class mask and this interface does not interpret a bit of it: the
* vocabulary that owns the bits (events today, sealing / lineage / lifecycle later) is the only
* thing that knows what they mean. `mode` says how to read the mask:
*
* CUBE_FLAG_ANY the record shares at least one bit with `mask` "every error"
* CUBE_FLAG_ALL the record carries every bit of `mask` "every Wi-Fi error"
*
* A `mask` of zero matches *nothing*, not everything: naming no class is asking no question, and a
* scan that answered a walk's worth of records to an empty question would be a walk wearing a
* scan's hat.
*
* The cursor counts **matches already returned**, as the region walk's counts records in its box,
* and for the same reason: the records examined before a match are not matches, so the index
* position of the cursor-th match is not arithmetic. A batch holds as many whole records as fit, so
* most batches come back short; reading a short batch as the end truncates the answer. A finished
* scan answers with no records and the cursor unchanged.
*
* Records come back as `key(24) | flags(2, little-endian) | value_len(u32, little-endian) | value`
* the walk's frame with the class mask in it. The mask travels because a scan's answer has to say
* what class each record answered with: a record can carry bits beyond the one asked for, and no
* other operation returns a mask.
*/
struct cube_flag_scan_args {
__u32 size; /* sizeof(struct cube_flag_scan_args) as the caller built it */
__u32 op; /* CUBE_OP_FLAG_SCAN */
__u8 space[32]; /* in: the space to scan */
__u16 mask; /* in: the class mask to match; 0 matches nothing */
__u16 mode; /* in: CUBE_FLAG_ANY or CUBE_FLAG_ALL */
__u64 cursor; /* in: 0 to start, or what the last call returned;
* out: what to pass next see the end-of-scan rule above
*/
__u64 value; /* user pointer: where to put the records */
__u64 len; /* in: the buffer's capacity;
* out: bytes written, or on -ERANGE what would be needed
*/
};
#define CUBE_FLAG_ANY 0 /* the record shares at least one bit with the mask */
#define CUBE_FLAG_ALL 1 /* the record carries every bit of the mask */
#endif /* _UAPI_LINUX_CUBE_H */