@pome-sh/checks 0.1.7 → 0.2.0

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,100 @@
1
1
  # @pome-sh/checks
2
2
 
3
+ ## 0.2.0 — 2026-08-13
4
+
5
+ Carries the vocabulary's first POSITIVE tape assertion on github to the grader
6
+ (F-1338). `GITHUB_CHECKS` goes 15 → 16, so `checksDigest` moves and every pin
7
+ must catch up — minor, for the same reason twin-github's 0.5.0 and 0.6.0 were.
8
+
9
+ - `github.tool-was-called` — template `` `{tool}` was called ``, substrate
10
+ `tape`, **positive** polarity. Matches on the recorded `tool` field, so it
11
+ asserts about the ACTION and not the transport: an examinee that acted over
12
+ `POST /repos/:owner/:repo/statuses/:sha` satisfies it exactly as one going
13
+ through `tools/call` does. It counts an ATTEMPT, the same question its
14
+ prohibition sibling answers — a call the twin rejected still called the
15
+ action, so this measures what the examinee reached for and never whether it
16
+ succeeded.
17
+
18
+ **Why it had to exist.** Every tape check github declared before this one is a
19
+ prohibition, and a prohibition cannot separate *"held the line"* from *"never
20
+ showed up"*: a do-nothing agent satisfies it by doing nothing. Six exam tasks
21
+ were cleared by a null agent, and no amount of negative vocabulary fixes any of
22
+ them.
23
+
24
+ **The slot is shared with `github.tool-never-called`, deliberately, and that is
25
+ the load-bearing part.** Both are generated from `TAPE_ASSERTABLE_TOOLS` — the
26
+ actions the recorder stamps on BOTH doors — because a criterion naming an
27
+ unstamped action is wrong in both directions for the identical missing fact:
28
+
29
+ | sentence | run performed the action by REST | verdict |
30
+ | -- | -- | -- |
31
+ | `` `X` was never called `` | `tool` is `null`, no match | `passed` — the negative false-pass D4 forbids |
32
+ | `` `X` was called `` | `tool` is `null`, no match | `failed` — a correct agent marked down |
33
+
34
+ One set, one invariant, and both sentences widen together the day a route is
35
+ stamped (F-1342). A second enumeration would be the one that drifts.
36
+
37
+ **Three things differ from the prohibition, each because the polarity flipped:**
38
+
39
+ 1. An EMPTY tape reaches a real `failed`, never a skip. `[]` is "the agent
40
+ called nothing", which is exactly the null agent this check exists to score
41
+ at 0; softening it would take the criterion out of the denominator and hand
42
+ that agent its score back.
43
+ 2. A tape whose rows carry no `tool` key AT ALL — a recording predating F-1125 —
44
+ is refused as `tool_not_recorded`. The prohibition can read that absence as
45
+ "not a match" and stay safe; reading it the same way here fails a correct
46
+ agent for the age of its tape.
47
+ 3. Citations move to the PASS branch. A positive pass has specific rows to point
48
+ at; a positive fail is an absence over the whole tape, with nothing to cite.
49
+
50
+ No existing check id, template, polarity or predicate changed, so no criterion
51
+ that binds today moves to a different check or a different verdict.
52
+
53
+ **What pome-cloud must do.** Pin `0.2.0` on **both** `apps/control-plane` and
54
+ `apps/mcp` — they pin this package exactly and must move together, or
55
+ `save_task` accepts criteria the grader cannot bind. Expect
56
+ `CORPUS_SHAPE_BASELINE` to move only once a task actually writes the new
57
+ sentence; this release adds vocabulary and edits no criterion.
58
+
59
+ **And one thing to CHECK rather than inherit, because its failure is silent.**
60
+ `github.tool-was-called` distinguishes `tool: null` ("this surface declares no
61
+ action" — a real world, and the null agent's) from `tool` ABSENT ("this
62
+ recording predates F-1125" — a skip). The twin writes `null` explicitly on every
63
+ unstamped surface, but `twinHttpEventSchema` types the field
64
+ `.nullable().optional()`, so a tape mapper or jsonb round-trip that DROPS
65
+ null-valued keys would make every read-only run look like a legacy recording —
66
+ and this check would `skipped` the null agent instead of failing it, which is
67
+ the one outcome it exists to prevent. A skip does not announce itself the way a
68
+ wrong verdict does. Confirm a persisted tape row for an unstamped call still
69
+ carries `"tool": null` when the pin lands.
70
+
71
+ ## 0.1.8
72
+
73
+ **Grading-vocabulary change: twin-github's seed gained
74
+ `repositories[].files[].renamed_from`.** No check declaration moves, no criterion
75
+ moves and `checksDigest` is unaffected — this package's other half is the seed
76
+ schemas, and this is one of them.
77
+
78
+ `renamed_from` names the path a file was MOVED from on `branch`, and it is the
79
+ only way a seed can take a path away from a branch. A seeded branch is created
80
+ from the default branch and inherits every path, and a plain `files[]` entry can
81
+ add or overwrite but never remove — so `status: "renamed"`, which
82
+ `PullRequestFileRow` has always declared, was reachable from no seed at all, and
83
+ `GET /pulls/:n/files` could not be made to serve GitHub's `previous_filename` by
84
+ any world. Registering that as an allowance would have been accepting a gap in
85
+ the twin's core file model.
86
+
87
+ `content` is refused alongside `renamed_from` and read from the source path
88
+ instead. That is not ergonomics: the branch diff detects a move by pairing
89
+ identical blobs, so a seed naming both a source and different content would be
90
+ asking for a rename the diff would report as an add plus a remove — the same
91
+ unreachability one level up. A seed that sets `renamed_from` with no `branch`, or
92
+ whose source is not a file on the branch, or whose source is its own destination,
93
+ is refused with a message naming the field.
94
+
95
+ Every seed valid before this is valid now: `content` became optional in the
96
+ object and required by refine wherever `renamed_from` is absent.
97
+
3
98
  ## 0.1.7
4
99
 
5
100
  A section a check's verdict reads is now measured HERE, where the worlds are
@@ -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
  }>];
@@ -23,8 +23,9 @@ export declare const seedSchema: z.ZodObject<{
23
23
  }, z.core.$strip>>>;
24
24
  files: z.ZodDefault<z.ZodArray<z.ZodObject<{
25
25
  path: z.ZodString;
26
- content: z.ZodString;
26
+ content: z.ZodOptional<z.ZodString>;
27
27
  branch: z.ZodOptional<z.ZodString>;
28
+ renamed_from: z.ZodOptional<z.ZodString>;
28
29
  }, z.core.$strip>>>;
29
30
  milestones: z.ZodDefault<z.ZodArray<z.ZodObject<{
30
31
  number: z.ZodOptional<z.ZodNumber>;
@@ -24,8 +24,9 @@ export type SeedRepository = {
24
24
  }>;
25
25
  files?: Array<{
26
26
  path: string;
27
- content: string;
27
+ content?: string;
28
28
  branch?: string;
29
+ renamed_from?: string;
29
30
  }>;
30
31
  milestones?: Array<{
31
32
  number?: number;
@@ -202,6 +203,7 @@ export type PullRequestFileRow = {
202
203
  raw_url: string;
203
204
  contents_url: string;
204
205
  patch: string;
206
+ previous_filename: string | null;
205
207
  };
206
208
  export type CommitStatusRow = {
207
209
  id: number;
@@ -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({
@@ -862,7 +933,52 @@ var seedSchema = z.object({
862
933
  color: z.string().default("ededed"),
863
934
  description: z.string().default("")
864
935
  })).default([]),
865
- files: z.array(z.object({ path: z.string().min(1), content: z.string(), branch: z.string().optional() })).default([]),
936
+ // F-1500 `renamed_from` is how a seed expresses a MOVE, and with it the
937
+ // `status: "renamed"` the row type has always declared and no world could
938
+ // reach. A seeded branch is created from the default branch and inherits
939
+ // every path, and a plain `files[]` entry can only add or overwrite, so
940
+ // before this there was no way to make a path ABSENT from the head branch
941
+ // — and `previous_filename` was therefore unreachable from any seed, not
942
+ // merely unemitted.
943
+ //
944
+ // `content` is refused alongside `renamed_from` rather than merged with
945
+ // it: the diff detects a move by pairing identical blobs (see
946
+ // `calculatePullFiles`), so a seed naming a source AND different content
947
+ // would be asking for a rename the diff would report as an add plus a
948
+ // remove. Refusing it keeps "the seed asked for a rename" and "the twin
949
+ // serves a rename" the same statement. The content comes from the source
950
+ // path, which the domain resolves on the branch the move happens on.
951
+ files: z.array(z.object({
952
+ path: z.string().min(1),
953
+ content: z.string().optional(),
954
+ branch: z.string().optional(),
955
+ renamed_from: z.string().min(1).optional()
956
+ }).superRefine((file, ctx) => {
957
+ if (file.renamed_from === void 0) {
958
+ if (file.content === void 0) {
959
+ ctx.addIssue({
960
+ code: "custom",
961
+ path: ["content"],
962
+ message: "content is required on a file entry that declares no renamed_from"
963
+ });
964
+ }
965
+ return;
966
+ }
967
+ if (file.content !== void 0) {
968
+ ctx.addIssue({
969
+ code: "custom",
970
+ path: ["content"],
971
+ message: `renamed_from carries the source file's content, so content must be omitted (${file.path})`
972
+ });
973
+ }
974
+ if (file.renamed_from === file.path) {
975
+ ctx.addIssue({
976
+ code: "custom",
977
+ path: ["renamed_from"],
978
+ message: `renamed_from must name a different path than the file it moves to (${file.path})`
979
+ });
980
+ }
981
+ })).default([]),
866
982
  // F-1421 — milestones, tags and releases are repository-level entities the
867
983
  // twin already SERVES (`GET /milestones`, `/tags`, `/releases`,
868
984
  // `/releases/latest`, `/releases/tags/:tag`) and the seed could not
package/dist/github.js CHANGED
@@ -1,2 +1,2 @@
1
- export { GITHUB_CHECKS, defaultSeedState, parseSeed, seedSchema } from './chunk-JVCGWAZQ.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-JVCGWAZQ.js';
3
- export { GITHUB_CHECKS, defaultSeedState as defaultGitHubSeed, seedSchema as githubSeedSchema, parseSeed as parseGitHubSeed } from './chunk-JVCGWAZQ.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.7",
3
+ "version": "0.2.0",
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",