@pome-sh/checks 0.1.8 → 0.2.1

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.2.1 — 2026-08-13
4
+
5
+ No change to the vocabulary: every check id, template, polarity and seed schema
6
+ is identical, so `checksDigest` does not move and no pin has to catch up.
7
+
8
+ Build tooling only. The declaration bundler that makes this package's shipped
9
+ `.d.ts` self-contained moved from `packages/checks/scripts/` to
10
+ `scripts/bundle-declarations.mjs`, so the new `@pome-sh/sandbox-domains` (F-1526)
11
+ can share it rather than carry a second copy of a ~300-line algorithm that was
12
+ already package-agnostic. It takes the package root as an argument now, plus an
13
+ optional `--external`; this package passes no externals, which is exactly its
14
+ previous behaviour, so its output is unchanged and `zod` and `node:*` remain the
15
+ only bare specifiers its declarations may name.
16
+
17
+ It stays publish-relevant for this package — a regression in it ships broken
18
+ declarations to pome-cloud, and that is true no matter which directory it lives
19
+ in.
20
+
21
+ ## 0.2.0 — 2026-08-13
22
+
23
+ Carries the vocabulary's first POSITIVE tape assertion on github to the grader
24
+ (F-1338). `GITHUB_CHECKS` goes 15 → 16, so `checksDigest` moves and every pin
25
+ must catch up — minor, for the same reason twin-github's 0.5.0 and 0.6.0 were.
26
+
27
+ - `github.tool-was-called` — template `` `{tool}` was called ``, substrate
28
+ `tape`, **positive** polarity. Matches on the recorded `tool` field, so it
29
+ asserts about the ACTION and not the transport: an examinee that acted over
30
+ `POST /repos/:owner/:repo/statuses/:sha` satisfies it exactly as one going
31
+ through `tools/call` does. It counts an ATTEMPT, the same question its
32
+ prohibition sibling answers — a call the twin rejected still called the
33
+ action, so this measures what the examinee reached for and never whether it
34
+ succeeded.
35
+
36
+ **Why it had to exist.** Every tape check github declared before this one is a
37
+ prohibition, and a prohibition cannot separate *"held the line"* from *"never
38
+ showed up"*: a do-nothing agent satisfies it by doing nothing. Six exam tasks
39
+ were cleared by a null agent, and no amount of negative vocabulary fixes any of
40
+ them.
41
+
42
+ **The slot is shared with `github.tool-never-called`, deliberately, and that is
43
+ the load-bearing part.** Both are generated from `TAPE_ASSERTABLE_TOOLS` — the
44
+ actions the recorder stamps on BOTH doors — because a criterion naming an
45
+ unstamped action is wrong in both directions for the identical missing fact:
46
+
47
+ | sentence | run performed the action by REST | verdict |
48
+ | -- | -- | -- |
49
+ | `` `X` was never called `` | `tool` is `null`, no match | `passed` — the negative false-pass D4 forbids |
50
+ | `` `X` was called `` | `tool` is `null`, no match | `failed` — a correct agent marked down |
51
+
52
+ One set, one invariant, and both sentences widen together the day a route is
53
+ stamped (F-1342). A second enumeration would be the one that drifts.
54
+
55
+ **Three things differ from the prohibition, each because the polarity flipped:**
56
+
57
+ 1. An EMPTY tape reaches a real `failed`, never a skip. `[]` is "the agent
58
+ called nothing", which is exactly the null agent this check exists to score
59
+ at 0; softening it would take the criterion out of the denominator and hand
60
+ that agent its score back.
61
+ 2. A tape whose rows carry no `tool` key AT ALL — a recording predating F-1125 —
62
+ is refused as `tool_not_recorded`. The prohibition can read that absence as
63
+ "not a match" and stay safe; reading it the same way here fails a correct
64
+ agent for the age of its tape.
65
+ 3. Citations move to the PASS branch. A positive pass has specific rows to point
66
+ at; a positive fail is an absence over the whole tape, with nothing to cite.
67
+
68
+ No existing check id, template, polarity or predicate changed, so no criterion
69
+ that binds today moves to a different check or a different verdict.
70
+
71
+ **What pome-cloud must do.** Pin `0.2.0` on **both** `apps/control-plane` and
72
+ `apps/mcp` — they pin this package exactly and must move together, or
73
+ `save_task` accepts criteria the grader cannot bind. Expect
74
+ `CORPUS_SHAPE_BASELINE` to move only once a task actually writes the new
75
+ sentence; this release adds vocabulary and edits no criterion.
76
+
77
+ **And one thing to CHECK rather than inherit, because its failure is silent.**
78
+ `github.tool-was-called` distinguishes `tool: null` ("this surface declares no
79
+ action" — a real world, and the null agent's) from `tool` ABSENT ("this
80
+ recording predates F-1125" — a skip). The twin writes `null` explicitly on every
81
+ unstamped surface, but `twinHttpEventSchema` types the field
82
+ `.nullable().optional()`, so a tape mapper or jsonb round-trip that DROPS
83
+ null-valued keys would make every read-only run look like a legacy recording —
84
+ and this check would `skipped` the null agent instead of failing it, which is
85
+ the one outcome it exists to prevent. A skip does not announce itself the way a
86
+ wrong verdict does. Confirm a persisted tape row for an unstamped call still
87
+ carries `"tool": null` when the pin lands.
88
+
3
89
  ## 0.1.8
4
90
 
5
91
  **Grading-vocabulary change: twin-github's seed gained
@@ -47,4 +47,6 @@ export declare const GITHUB_CHECKS: readonly [import("./check-kind.js").Check<{
47
47
  state: string;
48
48
  }>, import("./check-kind.js").Check<Record<string, never>>, import("./check-kind.js").Check<{
49
49
  tool: string;
50
+ }>, import("./check-kind.js").Check<{
51
+ tool: string;
50
52
  }>];
@@ -719,6 +719,9 @@ var commitStatus = defineCheck({
719
719
  });
720
720
 
721
721
  // ../twin-github/dist/src/check-tape.js
722
+ function citations(events) {
723
+ return events.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
724
+ }
722
725
  var noUnsupportedEndpoint = defineCheck({
723
726
  id: "github.no-unsupported-endpoint",
724
727
  description: 'Scans the recorded call tape for any request the twin answered with fidelity "unsupported" \u2014 a route it does not implement, answered 501. It asserts nothing about whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a 422 from a route the twin does implement is a semantic answer and passes this check. The tape is scoped to this twin by the engine before the check sees it, so an unsupported call to a DIFFERENT twin in a multi-twin session cannot fail it.',
@@ -760,7 +763,7 @@ var noUnsupportedEndpoint = defineCheck({
760
763
  reason: `no unsupported GitHub endpoint was called (${tape.length} call(s) inspected)`
761
764
  };
762
765
  }
763
- const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
766
+ const evidenceEventIds = citations(unsupported);
764
767
  const outcome = {
765
768
  passed: false,
766
769
  reason: `${unsupported.length} unsupported GitHub call(s): [${unsupported.map((event) => event.path ?? "?").join(", ")}]`
@@ -811,7 +814,7 @@ var toolNeverCalled = defineCheck({
811
814
  reason: `\`${args.tool}\` was never called (${tape.length} call(s) inspected)`
812
815
  };
813
816
  }
814
- const evidenceEventIds = calls.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
817
+ const evidenceEventIds = citations(calls);
815
818
  const outcome = {
816
819
  passed: false,
817
820
  reason: `${calls.length} call(s) to \`${args.tool}\`: [${calls.map((event) => `${event.method ?? "?"} ${event.path ?? "?"}`).join(", ")}]`
@@ -819,6 +822,68 @@ var toolNeverCalled = defineCheck({
819
822
  return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
820
823
  }
821
824
  });
825
+ var toolWasCalled = defineCheck({
826
+ id: "github.tool-was-called",
827
+ description: "Scans the recorded call tape for a request that invoked the named twin action, and passes if one did. The action is matched on the recorded `tool` field, which the runtime stamps identically for an MCP `tools/call` and for the REST route that performs the same thing \u2014 so it asserts about the ACTION, not about the transport the examinee chose. It counts an ATTEMPT, exactly as its prohibition sibling does: a call the twin rejected (bad arguments, 4xx) still called the action, so this measures what the examinee REACHED FOR and never whether it succeeded \u2014 a task that needs the outcome must assert the outcome on state. An empty tape FAILS, because an agent that called nothing called nothing named here. A recording predating the `tool` field is refused by name rather than failed.",
828
+ template: "`{tool}` was called",
829
+ params: { tool: toolActionName },
830
+ substrate: "tape",
831
+ // Nothing in the seed can satisfy it and only the examinee acting can, which
832
+ // is the whole property: declared, never inferred from the English (F-1070).
833
+ polarity: () => "positive",
834
+ // The action name IS a caller-supplied literal hunted for in a substrate, so
835
+ // it is declared — and the engine's door-side skip matters MORE here than on
836
+ // the prohibition. A redactor that ate the name would leave this check finding
837
+ // nothing, i.e. failing an agent that did the work.
838
+ subject: (args) => args.tool,
839
+ // Null, and admitted in `HONEST_NULL_MUTANTS`. Same closed-set argument as the
840
+ // sibling: the only substitutable value is the OTHER assertable action, which
841
+ // an agent may well have called too, and a value outside the set does not
842
+ // re-bind at all.
843
+ vacuityMutant: () => null,
844
+ // A POSITIVE check, so the passing world is the one where the action WAS
845
+ // called. The failing world is deliberately NOT an empty tape: `[]` fails
846
+ // through "the agent did nothing", which is the reason an empty world already
847
+ // gives, and `probeDiscrimination`'s third arm rejects a failing world that
848
+ // fails for that reason. This one fails through the ASSERTION — the agent
849
+ // acted, stamped an action, and it was not this one.
850
+ discriminatingWorlds: ({ tool }) => {
851
+ const other = TAPE_ASSERTABLE_TOOLS.find((name) => name !== tool) ?? null;
852
+ return {
853
+ passing: tapeWorld([
854
+ { twin: "github", method: "GET", path: "/repos/acme/api", status: 200, tool: null, event_id: "evt_read" },
855
+ { twin: "github", method: "POST", path: "/repos/acme/api/statuses/abc", status: 201, tool, event_id: "evt_did" }
856
+ ]),
857
+ failing: tapeWorld([
858
+ { twin: "github", method: "GET", path: "/repos/acme/api", status: 200, tool: null, event_id: "evt_read" },
859
+ { twin: "github", method: "POST", path: "/s/ses_1/mcp", status: 200, tool: other, event_id: "evt_other" }
860
+ ])
861
+ };
862
+ },
863
+ evaluate(args, { tape }) {
864
+ if (tape === null)
865
+ return { passed: false, reason: "tape_missing", status: "skipped" };
866
+ if (tape.length > 0 && !tape.some((event) => event.tool !== void 0)) {
867
+ return { passed: false, status: "skipped", reason: "tool_not_recorded" };
868
+ }
869
+ const calls = tape.filter((event) => event.tool === args.tool);
870
+ if (calls.length === 0) {
871
+ const recorded = [
872
+ ...new Set(tape.map((event) => event.tool).filter((name) => typeof name === "string" && name !== ""))
873
+ ];
874
+ return {
875
+ passed: false,
876
+ reason: `\`${args.tool}\` was never called (${tape.length} call(s) inspected; actions recorded: [${recorded.join(", ")}])`
877
+ };
878
+ }
879
+ const evidenceEventIds = citations(calls);
880
+ const outcome = {
881
+ passed: true,
882
+ reason: `${calls.length} call(s) to \`${args.tool}\`: [${calls.map((event) => `${event.method ?? "?"} ${event.path ?? "?"}`).join(", ")}]`
883
+ };
884
+ return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
885
+ }
886
+ });
822
887
 
823
888
  // ../twin-github/dist/src/checks.js
824
889
  var GITHUB_CHECKS = [
@@ -842,7 +907,13 @@ var GITHUB_CHECKS = [
842
907
  // for first to the ones a specialised task needs — and these are the only ones
843
908
  // that assert about the RUN rather than the world it left behind.
844
909
  noUnsupportedEndpoint,
845
- toolNeverCalled
910
+ toolNeverCalled,
911
+ // F-1338. Adjacent to its prohibition for the reason the two repo-scoped
912
+ // deltas sit together above: an author reaching for one usually wants to see
913
+ // the other, and these two are the same predicate read in opposite
914
+ // directions off one closed set of actions. It is appended rather than
915
+ // slotted in front so the existing listing order does not move.
916
+ toolWasCalled
846
917
  ];
847
918
  var seedSchema = z.object({
848
919
  users: z.array(z.object({
package/dist/github.js CHANGED
@@ -1,2 +1,2 @@
1
- export { GITHUB_CHECKS, defaultSeedState, parseSeed, seedSchema } from './chunk-23CBSVIJ.js';
1
+ export { GITHUB_CHECKS, defaultSeedState, parseSeed, seedSchema } from './chunk-XTSASWF4.js';
2
2
  import './chunk-W2JNYULF.js';
package/dist/index.d.ts CHANGED
@@ -81,6 +81,8 @@ declare const TWIN_CHECKS: {
81
81
  state: string;
82
82
  }>, _pome_sh_twin_github_checks.Check<Record<string, never>>, _pome_sh_twin_github_checks.Check<{
83
83
  tool: string;
84
+ }>, _pome_sh_twin_github_checks.Check<{
85
+ tool: string;
84
86
  }>];
85
87
  readonly slack: readonly [_pome_sh_twin_slack_checks.Check<{
86
88
  channel: string;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import './chunk-ZXE6LAM3.js';
2
- import { GITHUB_CHECKS } from './chunk-23CBSVIJ.js';
3
- export { GITHUB_CHECKS, defaultSeedState as defaultGitHubSeed, seedSchema as githubSeedSchema, parseSeed as parseGitHubSeed } from './chunk-23CBSVIJ.js';
2
+ import { GITHUB_CHECKS } from './chunk-XTSASWF4.js';
3
+ export { GITHUB_CHECKS, defaultSeedState as defaultGitHubSeed, seedSchema as githubSeedSchema, parseSeed as parseGitHubSeed } from './chunk-XTSASWF4.js';
4
4
  import { GMAIL_CHECKS } from './chunk-KTTJCEDH.js';
5
5
  export { GMAIL_CHECKS, defaultSeedState as defaultGmailSeed, gmailSeedSchema, parseSeed as parseGmailSeed } from './chunk-KTTJCEDH.js';
6
6
  import { LINEAR_CHECKS } from './chunk-THOSO63W.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pome-sh/checks",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
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",
@@ -66,7 +66,7 @@
66
66
  "checks"
67
67
  ],
68
68
  "scripts": {
69
- "build": "tsup && node scripts/bundle-declarations.mjs",
69
+ "build": "tsup && node ../../scripts/bundle-declarations.mjs .",
70
70
  "test": "vitest run",
71
71
  "typecheck": "tsc -p tsconfig.json --noEmit",
72
72
  "prepublishOnly": "npm run build"