@pome-sh/checks 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # @pome-sh/checks
2
2
 
3
+ ## 0.1.7
4
+
5
+ A section a check's verdict reads is now measured HERE, where the worlds are
6
+ authored, instead of being left to an instrument that cannot see it (F-1437).
7
+
8
+ pome-cloud's `findVacuousStateSectionReaders` derives its candidate sections
9
+ from what DIFFERS between a check's two discriminating worlds. That rule is what
10
+ stops it reporting every section a world carries for realism, and it is also a
11
+ blind spot it has documented rather than folded into its zero: a section
12
+ IDENTICAL in both worlds is never a candidate, so it is never deleted, so a
13
+ verdict that reads it vacuously is invisible there. No widening fixes that — with
14
+ both worlds carrying the same value there is no failing value to swap in, hence
15
+ no proof the verdict reads the section at all.
16
+
17
+ `test/section-read-sweep.test.ts` reaches it from the other side. It hands
18
+ `evaluate` a recording view of the state tree, so every top-level section the
19
+ predicate ASKS FOR is on the record whether or not the worlds disagree about it;
20
+ then, for exactly the sections the worlds agree on, it runs the detector's own
21
+ step 3 — delete from the passing world, re-evaluate — and a verdict that stays a
22
+ bare `passed` is named. Both trees, `seed` as well as `final`, because the
23
+ detector probes `final` only. Over the shipped vocabulary: 38 state
24
+ declarations and 34 agreed-on section reads (28 on `final`, 6 on `seed`), of
25
+ which 26 are discharged by measurement — 22 return `state_incomplete` and 4 fail
26
+ honestly — and 8 are declared exceptions where the twin reads an absent section
27
+ as a VALUE: the
28
+ `exportBounds` block, whose absence `isTruncated` deliberately answers as "not
29
+ truncated" so an export predating the cap is not skipped wholesale, and
30
+ `gmail.mailbox-label-count`'s `mailboxes`, where an absent collection means
31
+ "count by `mailboxEmail`" and the evidence collections are guarded by name one
32
+ line above. The exception list is pinned in both directions, so a row that stops
33
+ being needed fails the same way an unexplained finding does.
34
+
35
+ **ONE DECLARATION MOVED, AND IT IS A FIXTURE, NOT A VERDICT.**
36
+ `gmail.message-has-label`'s discriminating worlds built their user label as
37
+ `userLabel(label, label)` — an `id === name` shape only a SYSTEM label has. With
38
+ it, `labelIdsFor`'s bare-display-name fallback answered the join unaided, so
39
+ deleting `labels` from the passing world changed nothing and the verdict stayed
40
+ `passed`: a section the verdict demonstrably reads, proven unread by the only
41
+ world that speaks for it. The label now carries a minted id (`Label_1`) and the
42
+ `messageLabels` rows carry that id, so the deletion moves the verdict to
43
+ `failed` and the read is on the record.
44
+
45
+ Nothing in any `evaluate` changed. No check id, template, params, polarity,
46
+ subject or vacuity mutant moved, and no criterion changes from bound to unbound
47
+ or from passing to failing on any real export. `discriminatingWorlds` is a
48
+ fixture the probes run; a run's grade does not read it.
49
+
50
+ `labelIdsFor`'s comment counted TWO callers that must guard `state.labels`. There
51
+ are three — `messageCarriesLabel` in the same file is the third, reached by
52
+ `gmail.message-has-label`, and it answers `false` rather than refusing. That is
53
+ the safe direction for a check whose polarity is always positive, so it is left
54
+ alone and the comment now says which caller does which, with the sweep as the
55
+ thing that keeps it measured rather than assumed.
56
+
57
+ **What pome-cloud must do.** Pin `0.1.7` in `apps/control-plane` AND `apps/mcp`
58
+ — they must move together or `save_task` accepts criteria the grader cannot
59
+ bind — and restate the blind spot in `findVacuousStateSectionReaders`' doc
60
+ comment as a property of the declarations rather than of the detector, with
61
+ `docs/grading/a3-bucket-ledger.md` §5 item 9 following it. `declared-pin.test.ts`
62
+ needs no other edit: the detector's finding set over the new declarations is
63
+ byte-identical, because `gmail.message-has-label`'s worlds still differ on
64
+ `messageLabels` alone and deleting it still returns `state_incomplete`.
65
+
66
+ ## 0.1.6
67
+
68
+ `gmail.mailbox-label-count` now refuses instead of scoring a free pass (F-1441)
69
+ — the same class as 0.1.5's `slack.no-reaction-added`, found live in twin-gmail
70
+ on a worse criterion. Its polarity flips NEGATIVE at count 0, so the vacuous
71
+ pass handed a point to an agent that did the forbidden thing.
72
+
73
+ `labelIdsFor` read `state.labels ?? []` with no absence guard. The bare display
74
+ name is always added to the id set so the join survives a capped collection —
75
+ but that only holds for SYSTEM labels, where `id === name`. A USER label's
76
+ minted id differs from its display name by construction (the default seed ships
77
+ `{ id: "Label_follow_up", name: "Follow Up" }`), so with `labels` absent the
78
+ lookup degraded to a name no `messageLabels` row carries, the total came out 0,
79
+ and `0 === 0` passed over an export in which the agent DID apply the label.
80
+
81
+ `labels` is now guarded for both absence and truncation alongside `messages` and
82
+ `messageLabels`, in `gmail.mailbox-label-count` and in the second `labelIdsFor`
83
+ consumer (`oneMessagePerRecipient`, positive polarity and fail-closed today —
84
+ guarded anyway, because safe-by-polarity is how this class survives review).
85
+ `labelIdsFor`'s own comment now states that its bare-id fallback is
86
+ system-labels-only and that callers must guard; `draftRecipients` carries the
87
+ written reason its ticket asked for.
88
+
3
89
  ## 0.1.5
4
90
 
5
91
  `slack.no-reaction-added` now refuses instead of scoring a free pass (F-1159).
@@ -243,6 +243,10 @@ export type ReleaseRow = {
243
243
  prerelease: 0 | 1;
244
244
  author_login: string;
245
245
  created_at: string;
246
+ /** F-1459 — GitHub's release `updated_at`. Equal to `created_at` on a release
247
+ * that has never been edited, which is every release this twin serves: it has
248
+ * no release-update route. */
249
+ updated_at: string;
246
250
  published_at: string | null;
247
251
  };
248
252
  export type CheckRunRow = {
@@ -108,6 +108,36 @@ export declare function resolveMessage(state: GmailCheckState, id: string): Reso
108
108
  * The bare `wanted` is always included so the join still works when the `labels`
109
109
  * collection was capped away — a `messageLabels` row carries the bare id
110
110
  * regardless.
111
+ *
112
+ * ⚠️ THAT FALLBACK IS SYSTEM-LABELS-ONLY, and the sentence above used to stop
113
+ * short of saying so, which is why the gap read as intentional (F-1441). It
114
+ * holds when `id === name` — `INBOX`, `UNREAD`, `STARRED`. A USER label's
115
+ * minted id differs from its display name by construction (the default seed
116
+ * ships `{ id: "Label_follow_up", name: "Follow Up" }`), so with `labels`
117
+ * absent or capped this degrades to a name no `messageLabels` row carries and
118
+ * the join silently returns nothing.
119
+ *
120
+ * So: this function cannot refuse — it returns a Set, and an empty Set is
121
+ * indistinguishable from "the label was never applied". **Every caller must
122
+ * decide what an absent or capped `labels` collection means before calling**.
123
+ *
124
+ * THERE ARE THREE CALLERS, not the two F-1441 counted, and the third is in this
125
+ * very file:
126
+ * * `gmail.mailbox-label-count` and `gmail.one-message-per-recipient` in
127
+ * check-messages.ts REFUSE — `state_incomplete` / `collection_truncated` —
128
+ * because the first flips NEGATIVE at count 0, where an empty Set is a free
129
+ * point for an agent that did the forbidden thing.
130
+ * * `messageCarriesLabel` below, reached by `gmail.message-has-label`, does
131
+ * NOT refuse: it answers `false`, so an absent `labels` costs that check's
132
+ * agent a point rather than gifting one. Its polarity is always positive, so
133
+ * the direction is safe — but safe-by-polarity is how this class survives
134
+ * review, so it is measured rather than assumed. `section-read-sweep.test.ts`
135
+ * in @pome-sh/checks deletes `labels` from that check's own passing world on
136
+ * every run and asserts the verdict MOVES; before F-1437 it did not, because
137
+ * that world minted the label with `id === name` and the fallback above
138
+ * answered the join unaided.
139
+ *
140
+ * A fourth caller must pick one of those two and say which.
111
141
  */
112
142
  export declare function labelIdsFor(state: GmailCheckState, wanted: string): Set<string>;
113
143
  /**
@@ -134,5 +164,13 @@ export declare function resolveLabelByName(state: GmailCheckState, name: string)
134
164
  * alone would answer "no draft is addressed to anyone", always. A draft whose
135
165
  * message did not survive the export contributes nothing rather than throwing;
136
166
  * the caller decides whether that emptiness is answerable.
167
+ *
168
+ * ⚠️ Same shape as `labelIdsFor`, same rule, and the reason is written down
169
+ * rather than left to be rediscovered (F-1441): `state.messages ?? []` cannot
170
+ * tell an absent collection from one that holds no match, so this function
171
+ * cannot refuse either. It is safe TODAY only because its sole caller
172
+ * null-checks `final.messages` first (`check-drafts.ts:63`) — safe-by-caller,
173
+ * which is precisely how this class survives review. A second caller must
174
+ * guard, or this must be changed to return a `Resolved`.
137
175
  */
138
176
  export declare function draftRecipients(state: GmailCheckState, draft: GmailCheckStateDraft): string[];
@@ -5,16 +5,35 @@
5
5
  * and the CLI reads (`recorder-events.ts`), the OpenTelemetry-native extension
6
6
  * of that union (`otel/`), and the secret redactor applied on the way to disk
7
7
  * (`redaction.ts`). Every twin, the sdk, the adapter and the CLI depend on this
8
- * package; NOTHING here knows about sessions, tasks, runs or the cloud REST
9
- * surface — those live in `cli/src/contract/` (F-942).
8
+ * package; NOTHING ON THIS BARREL knows about sessions, tasks, runs or the
9
+ * cloud REST surface — those live in `cli/src/contract/` (F-942).
10
10
  *
11
- * Two SUBPATH-ONLY surfaces are deliberately absent from this barrel, because
12
- * only some consumers should pay to load them: `@pome-sh/wire/otel/fixtures`
13
- * (the golden-fixture corpus, a test/dev artifact) and
14
- * `@pome-sh/wire/correlation` (F-950 the agent-side AsyncLocalStorage +
15
- * fetch-patching plumbing that stamps `x-pome-correlation-id`; importing it
16
- * constructs an AsyncLocalStorage, and no twin is the agent side of that
17
- * protocol). `test/export-surface.test.ts` pins both halves of that call.
11
+ * F-1416 NARROWED THAT CLAIM ONCE, and this is the whole of the narrowing: the
12
+ * package now also ships `@pome-sh/wire/run-completeness`, four symbols that
13
+ * read two fields of a `criteria_results` row (`skipped`, `reason`) and return
14
+ * whether a finished run has a verdict to state. That is the one predicate
15
+ * pome-cloud's dashboard and control plane and this repo's CLI all have to
16
+ * agree on, and until F-1416 the CLI's agreement test kept a hand-written copy
17
+ * of it that went stale green. It is run-ADJACENT, so the sentence above would
18
+ * be false if it said "this package"; it is not run vocabulary, because it
19
+ * names no session, task, run id, REST route or column, imports nothing, and
20
+ * takes structural inputs so no cloud type crosses with it. Keeping it OFF this
21
+ * barrel is what keeps the sentence above enforceable rather than merely
22
+ * written down — see below.
23
+ *
24
+ * Three SUBPATH-ONLY surfaces are deliberately absent from this barrel.
25
+ * `@pome-sh/wire/otel/fixtures` (the golden-fixture corpus, a test/dev
26
+ * artifact) and `@pome-sh/wire/correlation` (F-950 — the agent-side
27
+ * AsyncLocalStorage + fetch-patching plumbing that stamps
28
+ * `x-pome-correlation-id`) are absent because only some consumers should pay to
29
+ * LOAD them: importing correlation constructs an AsyncLocalStorage, and no twin
30
+ * is the agent side of that protocol. `@pome-sh/wire/run-completeness` (F-1416)
31
+ * is absent for a different reason — SCOPE, not cost. The five twins, the sdk
32
+ * and the adapter import this barrel and not one of them has a run to ask
33
+ * about, so a symbol they cannot use has no business in their namespace, and an
34
+ * opt-in subpath is what makes the F-942 boundary above a thing CI can check.
35
+ * `test/export-surface.test.ts` pins all three: each subpath's own surface, and
36
+ * its absence from the snapshot below.
18
37
  *
19
38
  * This file is a THIN BARREL: it re-exports only.
20
39
  */
@@ -373,10 +373,14 @@ var messageHasLabel = defineCheck({
373
373
  // lookup rather than the assertion. The label is the scanned literal.
374
374
  vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
375
375
  discriminatingWorlds: ({ message: id2, label }) => {
376
- const labels = [systemLabel("INBOX"), userLabel(label, label)];
376
+ const labelId = "Label_1";
377
+ const labels = [systemLabel("INBOX"), userLabel(labelId, label)];
377
378
  const base = { messages: [message(id2)], labels };
378
379
  return {
379
- passing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX"), messageLabel(id2, label)] })),
380
+ passing: finalWorld(gmailState({
381
+ ...base,
382
+ messageLabels: [messageLabel(id2, "INBOX"), messageLabel(id2, labelId)]
383
+ })),
380
384
  failing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX")] }))
381
385
  };
382
386
  },
@@ -430,10 +434,10 @@ var mailboxLabelCount = defineCheck({
430
434
  },
431
435
  evaluate({ mailbox, count, label }, { final }) {
432
436
  const wanted = Number(count);
433
- if (final.messages == null || final.messageLabels == null) {
437
+ if (final.messages == null || final.messageLabels == null || final.labels == null) {
434
438
  return { passed: false, status: "skipped", reason: "state_incomplete" };
435
439
  }
436
- if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
440
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels") || isTruncated(final, "labels")) {
437
441
  return { passed: false, status: "skipped", reason: "collection_truncated" };
438
442
  }
439
443
  if (final.mailboxes != null && !final.mailboxes.some((mb) => (mb.email ?? "").toLowerCase() === mailbox.toLowerCase())) {
@@ -490,10 +494,10 @@ var oneMessagePerRecipient = defineCheck({
490
494
  },
491
495
  evaluate({ label, count }, { final }) {
492
496
  const wanted = parseCount(count);
493
- if (final.messages == null || final.messageLabels == null) {
497
+ if (final.messages == null || final.messageLabels == null || final.labels == null) {
494
498
  return { passed: false, status: "skipped", reason: "state_incomplete" };
495
499
  }
496
- if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
500
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels") || isTruncated(final, "labels")) {
497
501
  return { passed: false, status: "skipped", reason: "collection_truncated" };
498
502
  }
499
503
  const ids = labelIdsFor(final, label);
package/dist/gmail.js CHANGED
@@ -1,2 +1,2 @@
1
- export { GMAIL_CHECKS, defaultSeedState, gmailSeedSchema, parseSeed } from './chunk-SA23X6PB.js';
1
+ export { GMAIL_CHECKS, defaultSeedState, gmailSeedSchema, parseSeed } from './chunk-KTTJCEDH.js';
2
2
  import './chunk-W2JNYULF.js';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import './chunk-ZXE6LAM3.js';
2
2
  import { GITHUB_CHECKS } from './chunk-JVCGWAZQ.js';
3
3
  export { GITHUB_CHECKS, defaultSeedState as defaultGitHubSeed, seedSchema as githubSeedSchema, parseSeed as parseGitHubSeed } from './chunk-JVCGWAZQ.js';
4
- import { GMAIL_CHECKS } from './chunk-SA23X6PB.js';
5
- export { GMAIL_CHECKS, defaultSeedState as defaultGmailSeed, gmailSeedSchema, parseSeed as parseGmailSeed } from './chunk-SA23X6PB.js';
4
+ import { GMAIL_CHECKS } from './chunk-KTTJCEDH.js';
5
+ export { GMAIL_CHECKS, defaultSeedState as defaultGmailSeed, gmailSeedSchema, parseSeed as parseGmailSeed } from './chunk-KTTJCEDH.js';
6
6
  import { LINEAR_CHECKS } from './chunk-THOSO63W.js';
7
7
  export { LINEAR_CHECKS, defaultSeedState as defaultLinearSeed, linearSeedSchema, parseSeed as parseLinearSeed } from './chunk-THOSO63W.js';
8
8
  import { SLACK_CHECKS } from './chunk-MJOHK2NI.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pome-sh/checks",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Pome's grading vocabulary — the check declarations, seed schemas and default seeds of all five digital twins, plus the check DSL they are written in. Declarations only: no twin server, no database, no routes, no tools.",
5
5
  "private": false,
6
6
  "type": "module",