@valbuild/shared 0.118.0 → 0.121.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,159 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.121.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#464](https://github.com/valbuild/val/pull/464) [`2bcc6fd`](https://github.com/valbuild/val/commit/2bcc6fdff8d668123e07e3c5e81ac6fa1436e47b) Thanks [@freekh](https://github.com/freekh)! - Add staging and unstaging of pending changes, so one person can publish a small fix without shipping somebody else's unfinished work.
8
+
9
+ A **patch group** is the set of patches one user has chosen to publish. It is not a patch _set_: a patch set is computed from the schema and says which patches must move together, while a patch group is curated and says which ones you want live.
10
+
11
+ A group holds its owner's own work plus whatever the closure entangled with it — not everything pending. **So Publish changes meaning on a shared branch: it ships your changes and what they depend on, instead of everything anybody has pending.** That is the feature. Unstaging goes further: hold one of your own changes back and it leaves both your preview and your publish, while still existing for everyone else.
12
+
13
+ The rule relating the two is that for every group and every patch set, the group's members within that patch set must form a prefix in patch-chain order. Staging a change therefore pulls in whatever preceded it in the same patch set; unstaging drops whatever was built on top of it. The compare view names what a toggle moves, and whose it is, rather than quietly enlarging or shrinking a publish.
14
+
15
+ Editing inside a region you are holding back is allowed, and the patches you were holding are loaded back in rather than the edit being refused. An earlier design made such a region read-only until it was staged again, because an author picks an array index while looking at their own view — so re-staging patches afterwards can shift the content under the path they just chose, and their edit lands on the wrong element cleanly, with every invariant intact and only the content wrong. That guard is not what ships. It is a rare shape in practice, since two people's edits mostly land in different routes, and refusing an edit for a reason the author cannot see is a worse everyday experience than the case it prevents. Instead the real result is shown immediately: the widened set is what the editor renders and what the compare view lists.
16
+
17
+ Also fixes a pre-existing bug in patch set grouping: patch set paths were compared with a raw string prefix test, and nothing terminates a path segment, so `?foobar/title` matched `?foo`. Deleting record key `foo` and retitling record key `foobar` were treated as one inseparable change. Previously that over-grouped two unrelated edits in the review screen; with staging it would have meant publishing a deletion nobody asked for.
18
+
19
+ The `/patches` routes gain optional patch group fields and `/patch-groups/~/patches` is new. This needs a content API that has patch groups. Filesystem mode keeps the group in the client, since it has a single author and already sends an explicit patch id list when publishing.
20
+
21
+ When a save pulls other people's changes in, you are told: a toast names how many and whose. There is no undo, because your edit was written against the view those changes produce and now depends on them — the compare view shows the widened set.
22
+
23
+ Two other things keep a session honest about a shared branch. `/stat` now says which pending changes have already been published, so another author's publish stops looking pending in your Studio the moment it lands rather than when the site redeploys. And Publish refuses, without writing anything, if somebody published while you were reviewing — the review screen you acted on described a branch that has since moved.
24
+
25
+ Two things this does **not** do yet, both of which need the group annotation to refresh on its own rather than only inside a fetch for missing patch ids:
26
+
27
+ - a stage or unstage in one tab does not reach another tab;
28
+ - if persisting a stage fails, the local view keeps it until the page is reloaded.
29
+
30
+ `docs/independent-publish/DESIGN.md` describes the model and lists what is still a judgement call.
31
+
32
+ - [#605](https://github.com/valbuild/val/pull/605) [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79) Thanks [@freekh](https://github.com/freekh)! - `s.settings()`: the project's settings, as content.
33
+
34
+ A settings module is one per project, at the root of the content tree:
35
+
36
+ ```typescript
37
+ // settings.val.ts
38
+ export default c.define("/settings.val.ts", s.settings(), {});
39
+ ```
40
+
41
+ Register it in `val.modules.ts` like any other module, and it shows up in the
42
+ Studio under the cog at the foot of the left rail. Everything in it is content:
43
+ it is edited as a draft, it appears in the publish diff, and it is the same for
44
+ everyone working on the project.
45
+
46
+ Every key is optional, at every level, so `{}` is a complete settings module —
47
+ and stays one as sections are added. What it holds today is the assistant:
48
+
49
+ ```typescript
50
+ export default c.define("/settings.val.ts", s.settings(), {
51
+ assistant: {
52
+ enabled: true,
53
+ context: "A CMS for developers, run by a team of four in Oslo.",
54
+ tone: "Plain and direct. British English, sentence case in headings.",
55
+ },
56
+ });
57
+ ```
58
+
59
+ `context` is background the assistant would otherwise guess at; `tone` is how it
60
+ should write when it writes content. Both are sent with every message it makes.
61
+
62
+ `enabled` decides whether editors have an assistant, and it has **three** states
63
+ rather than two:
64
+
65
+ - `true` — they do.
66
+ - `false` — they do not, and every trace of it goes: no button in the top bar,
67
+ no row in the quick actions, no panel, nothing sent.
68
+ - unset — nobody has decided. The assistant is still **shown**, and asks to be
69
+ turned on before it is used. Hiding an assistant nobody has decided about
70
+ means nobody discovers it; quietly enabling one means a project starts sending
71
+ its content to a model because it did not know to say no.
72
+
73
+ A project with no settings module at all has an assistant, as before: there is
74
+ nowhere to record a decision, and nowhere for the prompt to write the answer.
75
+
76
+ **Breaking: `ai.chat` is gone from `val.config.ts`.** Whether the assistant is
77
+ available is a decision about the project's content, made by the people who edit
78
+ it, so it moved to settings — turning the chat on used to take a developer, a
79
+ deploy and a code review of a boolean. Remove the whole block:
80
+
81
+ ```diff
82
+ const { s, c, val, config } = initVal({
83
+ - ai: {
84
+ - chat: {
85
+ - experimental: { enable: true },
86
+ - suggestions: ["Summarize", "Fix typos at this page"],
87
+ - title: "Ask me anything",
88
+ - description: "Val can answer questions about the content.",
89
+ - },
90
+ - },
91
+ });
92
+ ```
93
+
94
+ `experimental.enable` becomes `assistant.enabled` in the settings module.
95
+ `suggestions`, `title` and `description` are removed with nothing replacing
96
+ them: the assistant now opens with its own copy. A project that had the chat
97
+ enabled and wants it to stay on for everyone should write
98
+ `assistant: { enabled: true }` — otherwise editors are offered it and asked.
99
+
100
+ `ai.commitMessages` stays in `val.config.ts`, and is unaffected.
101
+
102
+ Two settings modules, or one in a subdirectory, is a module error: the dev
103
+ server refuses to serve sources, `npx val validate` reports it against the file,
104
+ and the Studio says so rather than picking one.
105
+
106
+ ### Patch Changes
107
+
108
+ - Updated dependencies [[`2bcbee1`](https://github.com/valbuild/val/commit/2bcbee1be682c2bbd5b7bc7d152ddd4204162fd2), [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79)]:
109
+ - @valbuild/core@0.121.0
110
+
111
+ ## 0.120.0
112
+
113
+ ### Minor Changes
114
+
115
+ - [#589](https://github.com/valbuild/val/pull/589) [`c2d3c0e`](https://github.com/valbuild/val/commit/c2d3c0e6c2010c0a94c725d9dbaa618998773e8a) Thanks [@freekh](https://github.com/freekh)! - **Breaking:** `s.richtext()` options are flat.
116
+
117
+ The `style`, `block` and `inline` groups are gone — every option is a key of its
118
+ own. The names are unchanged, so updating a schema is only a matter of removing
119
+ the wrappers:
120
+
121
+ ```ts
122
+ // before
123
+ s.richtext({
124
+ style: { bold: true, italic: true },
125
+ block: { h1: true, ul: true },
126
+ inline: { a: true, img: s.image() },
127
+ });
128
+
129
+ // after
130
+ s.richtext({
131
+ bold: true,
132
+ italic: true,
133
+ h1: true,
134
+ ul: true,
135
+ a: true,
136
+ img: s.image(),
137
+ });
138
+ ```
139
+
140
+ The groups never carried any meaning the option names did not already have, and
141
+ they cost something real: an option name and its `ValRichText` theme key were
142
+ spelled differently (`block.h1` vs `theme.h1`), so the type that keeps a theme
143
+ exhaustive had to restate all thirteen options by hand. It is now a mapped type
144
+ over the options themselves — which also fixes an inconsistency in it: enabling
145
+ links with a schema (`a: s.route()`) rather than `a: true` now requires an `a`
146
+ key in the theme, the way `img` always has.
147
+
148
+ `ValRichText` themes were already flat and are unchanged. The serialized schema
149
+ that the server sends the Studio is flat too, so a project must not mix
150
+ `@valbuild/*` versions across this release.
151
+
152
+ ### Patch Changes
153
+
154
+ - Updated dependencies [[`c2d3c0e`](https://github.com/valbuild/val/commit/c2d3c0e6c2010c0a94c725d9dbaa618998773e8a)]:
155
+ - @valbuild/core@0.120.0
156
+
3
157
  ## 0.118.0
4
158
 
5
159
  ### Minor Changes
@@ -1,6 +1,21 @@
1
1
  import { z } from "zod";
2
2
  import { type ModuleFilePath } from "@valbuild/core";
3
3
  declare const ModuleFilePath: z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>;
4
+ /**
5
+ * A patch group: the set of patches one user has chosen to publish.
6
+ *
7
+ * Not a patch *set* — a patch set is computed from the schema and says which
8
+ * patches must move together; a patch group is curated and says which ones this
9
+ * user wants live. See `docs/independent-publish/PLAN.md`.
10
+ */
11
+ export declare const PatchGroup: z.ZodObject<{
12
+ patchGroupId: z.ZodString;
13
+ authorId: z.ZodNullable<z.ZodString>;
14
+ createdAt: z.ZodString;
15
+ publishedAt: z.ZodNullable<z.ZodString>;
16
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
17
+ }, z.core.$strip>;
18
+ export type PatchGroupT = z.infer<typeof PatchGroup>;
4
19
  declare const cookies: z.ZodUnion<readonly [z.ZodLiteral<"val_session">, z.ZodLiteral<"val_enable">, z.ZodLiteral<"val_state">]>;
5
20
  type Cookies = z.infer<typeof cookies>;
6
21
  declare const enableCookieValue: z.ZodObject<{
@@ -430,6 +445,8 @@ export declare const Api: {
430
445
  schemaSha: z.ZodString;
431
446
  sourcesSha: z.ZodString;
432
447
  patches: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
448
+ appliedPatches: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
449
+ headCommitSha: z.ZodOptional<z.ZodString>;
433
450
  removed: z.ZodOptional<z.ZodArray<z.ZodObject<{
434
451
  patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
435
452
  reason: z.ZodString;
@@ -455,6 +472,8 @@ export declare const Api: {
455
472
  sourcesSha: z.ZodString;
456
473
  commitSha: z.ZodString;
457
474
  patches: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
475
+ appliedPatches: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
476
+ headCommitSha: z.ZodOptional<z.ZodString>;
458
477
  commits: z.ZodArray<z.ZodObject<{
459
478
  commitSha: z.ZodString;
460
479
  clientCommitSha: z.ZodString;
@@ -551,6 +570,9 @@ export declare const Api: {
551
570
  query: {
552
571
  id: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
553
572
  };
573
+ body: z.ZodOptional<z.ZodObject<{
574
+ unstagePatchIds: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
575
+ }, z.core.$strip>>;
554
576
  cookies: {
555
577
  val_session: z.ZodOptional<z.ZodString>;
556
578
  };
@@ -593,6 +615,8 @@ export declare const Api: {
593
615
  patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
594
616
  patch: z.ZodAny;
595
617
  }, z.core.$strip>>;
618
+ patchGroupId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
619
+ withPatchIds: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
596
620
  }, z.core.$strip>;
597
621
  cookies: {
598
622
  val_session: z.ZodOptional<z.ZodString>;
@@ -631,6 +655,7 @@ export declare const Api: {
631
655
  type: z.ZodLiteral<"patch">;
632
656
  patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
633
657
  }, z.core.$strip>]>;
658
+ patchGroupId: z.ZodOptional<z.ZodString>;
634
659
  }, z.core.$strip>;
635
660
  }, z.core.$strip>]>;
636
661
  };
@@ -639,6 +664,15 @@ export declare const Api: {
639
664
  query: {
640
665
  patch_id: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
641
666
  exclude_patch_ops: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
667
+ /**
668
+ * Include the patch group annotation in the answer.
669
+ *
670
+ * Off by default because the client CHUNKS this request — a long
671
+ * chain is several round trips — and the groups are a property of the
672
+ * branch, identical in every chunk. Asking on each one is N identical
673
+ * lookups against the content API to learn the same thing.
674
+ */
675
+ include_patch_groups: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
642
676
  };
643
677
  cookies: {
644
678
  val_session: z.ZodOptional<z.ZodString>;
@@ -712,7 +746,15 @@ export declare const Api: {
712
746
  appliedAt: z.ZodNullable<z.ZodObject<{
713
747
  commitSha: z.ZodString;
714
748
  }, z.core.$strip>>;
749
+ patchGroupIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
715
750
  }, z.core.$strip>>;
751
+ patchGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
752
+ patchGroupId: z.ZodString;
753
+ authorId: z.ZodNullable<z.ZodString>;
754
+ createdAt: z.ZodString;
755
+ publishedAt: z.ZodNullable<z.ZodString>;
756
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
757
+ }, z.core.$strip>>>;
716
758
  baseSha: z.ZodString;
717
759
  error: z.ZodOptional<z.ZodObject<{
718
760
  message: z.ZodString;
@@ -724,6 +766,86 @@ export declare const Api: {
724
766
  }, z.core.$strip>]>;
725
767
  };
726
768
  };
769
+ "/patch-groups/~/patches": {
770
+ PUT: {
771
+ req: {
772
+ body: z.ZodObject<{
773
+ patchGroupId: z.ZodString;
774
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
775
+ withPatchIds: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
776
+ }, z.core.$strip>;
777
+ cookies: {
778
+ val_session: z.ZodOptional<z.ZodString>;
779
+ };
780
+ };
781
+ res: z.ZodUnion<readonly [z.ZodObject<{
782
+ status: z.ZodLiteral<401>;
783
+ json: z.ZodObject<{
784
+ message: z.ZodString;
785
+ }, z.core.$strip>;
786
+ }, z.core.$strip>, z.ZodObject<{
787
+ status: z.ZodLiteral<403>;
788
+ json: z.ZodObject<{
789
+ message: z.ZodString;
790
+ }, z.core.$strip>;
791
+ }, z.core.$strip>, z.ZodObject<{
792
+ status: z.ZodLiteral<409>;
793
+ json: z.ZodObject<{
794
+ message: z.ZodString;
795
+ }, z.core.$strip>;
796
+ }, z.core.$strip>, z.ZodObject<{
797
+ status: z.ZodLiteral<500>;
798
+ json: z.ZodObject<{
799
+ message: z.ZodString;
800
+ }, z.core.$strip>;
801
+ }, z.core.$strip>, z.ZodObject<{
802
+ status: z.ZodLiteral<200>;
803
+ json: z.ZodObject<{
804
+ patchGroupId: z.ZodString;
805
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
806
+ }, z.core.$strip>;
807
+ }, z.core.$strip>]>;
808
+ };
809
+ DELETE: {
810
+ req: {
811
+ body: z.ZodObject<{
812
+ patchGroupId: z.ZodString;
813
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
814
+ withPatchIds: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
815
+ }, z.core.$strip>;
816
+ cookies: {
817
+ val_session: z.ZodOptional<z.ZodString>;
818
+ };
819
+ };
820
+ res: z.ZodUnion<readonly [z.ZodObject<{
821
+ status: z.ZodLiteral<401>;
822
+ json: z.ZodObject<{
823
+ message: z.ZodString;
824
+ }, z.core.$strip>;
825
+ }, z.core.$strip>, z.ZodObject<{
826
+ status: z.ZodLiteral<403>;
827
+ json: z.ZodObject<{
828
+ message: z.ZodString;
829
+ }, z.core.$strip>;
830
+ }, z.core.$strip>, z.ZodObject<{
831
+ status: z.ZodLiteral<409>;
832
+ json: z.ZodObject<{
833
+ message: z.ZodString;
834
+ }, z.core.$strip>;
835
+ }, z.core.$strip>, z.ZodObject<{
836
+ status: z.ZodLiteral<500>;
837
+ json: z.ZodObject<{
838
+ message: z.ZodString;
839
+ }, z.core.$strip>;
840
+ }, z.core.$strip>, z.ZodObject<{
841
+ status: z.ZodLiteral<200>;
842
+ json: z.ZodObject<{
843
+ patchGroupId: z.ZodString;
844
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
845
+ }, z.core.$strip>;
846
+ }, z.core.$strip>]>;
847
+ };
848
+ };
727
849
  "/schema": {
728
850
  GET: {
729
851
  req: {
@@ -771,6 +893,33 @@ export declare const Api: {
771
893
  validate_binary_files: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
772
894
  exclude_patches: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
773
895
  apply_patches: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
896
+ /**
897
+ * Apply ONLY these patches, rather than everything pending.
898
+ *
899
+ * This is what makes a draft-mode render match the caller's staged
900
+ * view. Without it `/sources/~` replays every pending patch on the
901
+ * branch, so a server-rendered preview shows other people's
902
+ * unpublished work — the one thing independent publish is for.
903
+ *
904
+ * Omitted means everything pending, which is the behaviour every
905
+ * existing caller has and must keep. An EMPTY array is a different
906
+ * answer and is honoured as one: a group holding nothing renders
907
+ * base.
908
+ */
909
+ patch_id: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>>;
910
+ /**
911
+ * Apply only the patches in the CALLER'S OWN patch groups.
912
+ *
913
+ * The server resolves them from the session, so a draft render does
914
+ * not have to know its own group ids — which it cannot, since it has
915
+ * no client state. This is what makes `fetchVal` in draft mode show
916
+ * the person their own staged work rather than everybody's.
917
+ *
918
+ * Ignored when `patch_id` is given: an explicit list is a caller that
919
+ * already knows what it wants, and silently intersecting the two
920
+ * would make the narrower one invisible.
921
+ */
922
+ own_patch_groups_only: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
774
923
  };
775
924
  cookies: {
776
925
  val_session: z.ZodOptional<z.ZodString>;
@@ -876,6 +1025,8 @@ export declare const Api: {
876
1025
  body: z.ZodObject<{
877
1026
  message: z.ZodOptional<z.ZodString>;
878
1027
  patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
1028
+ patchGroupId: z.ZodOptional<z.ZodString>;
1029
+ expectedHeadCommitSha: z.ZodOptional<z.ZodString>;
879
1030
  }, z.core.$strip>;
880
1031
  cookies: {
881
1032
  val_session: z.ZodOptional<z.ZodString>;
@@ -889,6 +1040,7 @@ export declare const Api: {
889
1040
  }, z.core.$strip>, z.ZodObject<{
890
1041
  status: z.ZodLiteral<200>;
891
1042
  json: z.ZodObject<{
1043
+ commitSha: z.ZodOptional<z.ZodString>;
892
1044
  removed: z.ZodOptional<z.ZodArray<z.ZodObject<{
893
1045
  patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
894
1046
  moduleFilePath: z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>;
@@ -896,11 +1048,29 @@ export declare const Api: {
896
1048
  }, z.core.$strip>>>;
897
1049
  }, z.core.$strip>;
898
1050
  }, z.core.$strip>, z.ZodObject<{
899
- status: z.ZodLiteral<409>;
1051
+ status: z.ZodLiteral<403>;
1052
+ json: z.ZodObject<{
1053
+ message: z.ZodString;
1054
+ }, z.core.$strip>;
1055
+ }, z.core.$strip>, z.ZodObject<{
1056
+ status: z.ZodLiteral<500>;
900
1057
  json: z.ZodObject<{
901
1058
  message: z.ZodString;
902
- isNotFastForward: z.ZodLiteral<true>;
903
1059
  }, z.core.$strip>;
1060
+ }, z.core.$strip>, z.ZodObject<{
1061
+ status: z.ZodLiteral<409>;
1062
+ json: z.ZodUnion<readonly [z.ZodObject<{
1063
+ message: z.ZodString;
1064
+ }, z.core.$strip>, z.ZodObject<{
1065
+ message: z.ZodString;
1066
+ isNotFastForward: z.ZodLiteral<true>;
1067
+ }, z.core.$strip>, z.ZodObject<{
1068
+ message: z.ZodString;
1069
+ headMoved: z.ZodLiteral<true>;
1070
+ }, z.core.$strip>, z.ZodObject<{
1071
+ message: z.ZodString;
1072
+ patchGroupPublished: z.ZodLiteral<true>;
1073
+ }, z.core.$strip>]>;
904
1074
  }, z.core.$strip>, z.ZodObject<{
905
1075
  status: z.ZodLiteral<400>;
906
1076
  json: z.ZodUnion<readonly [z.ZodObject<{
@@ -965,6 +1135,15 @@ export declare const Api: {
965
1135
  offset: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>, z.ZodTransform<number, string>>>;
966
1136
  limit: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>, z.ZodTransform<number, string>>>;
967
1137
  apply_patches: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
1138
+ /**
1139
+ * The caller's own staged work, and nobody else's.
1140
+ *
1141
+ * The same flag `/sources/~` takes, and for the same reason — a draft
1142
+ * page renders both, so a scoped module beside an unscoped
1143
+ * `jsonValues` entry showed one person's view and everybody's pending
1144
+ * work on the same screen.
1145
+ */
1146
+ own_patch_groups_only: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>> & z.ZodType<"true" | "false", string[], z.core.$ZodTypeInternals<"true" | "false", string[]>>, z.ZodTransform<boolean, "true" | "false">>>;
968
1147
  };
969
1148
  cookies: {
970
1149
  val_session: z.ZodOptional<z.ZodString>;
@@ -3,6 +3,7 @@ export * from "./server/types.js";
3
3
  export * from "./ValClient.js";
4
4
  export * from "./ValUrls.js";
5
5
  export * from "./ApiRoutes.js";
6
+ export * from "./newestCommitSha.js";
6
7
  export * from "./zod/Patch.js";
7
8
  export * from "./sessionStorage.js";
8
9
  export * from "./SharedValConfig.js";
@@ -0,0 +1,20 @@
1
+ import type { ValCommit } from "./zod/ValCommit.js";
2
+ /**
3
+ * The newest commit the client has been told about, or `null` if none.
4
+ *
5
+ * This is the **publish head**: unlike `baseSha`, which only moves once a
6
+ * deployment lands, it moves the instant somebody publishes. That makes it the
7
+ * one thing a client can carry to `/save` to say "this is the world I decided
8
+ * against" — and the only way the server can tell that somebody else has
9
+ * published since the review screen was read.
10
+ *
11
+ * In `shared` because both sides need the SAME answer: the client names a head
12
+ * and the server compares against one, so two implementations of "newest" is two
13
+ * ways to disagree about whether a publish is stale.
14
+ *
15
+ * By `createdAt` rather than by position, because the list's order is the
16
+ * content API's business and nothing here should depend on it. Ties keep the
17
+ * first seen, which is arbitrary and does not matter: two commits sharing a
18
+ * timestamp are equally "since", and either answer refuses the same publishes.
19
+ */
20
+ export declare function newestCommitSha(commits: readonly ValCommit[] | undefined): string | null;