@authorbot/domain 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Mattie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Annotation policy (Phase 7 contract "Restricting") — who may write to a
3
+ * book, and whether what they write appears immediately.
4
+ *
5
+ * | Mode | Who may write | Appears |
6
+ * |---------------------|--------------------------|----------------------------|
7
+ * | `open` | any signed-in user | immediately |
8
+ * | `approval-gated` | any signed-in user | after a maintainer approves|
9
+ * | `collaborators-only`| members only | immediately *(default)* |
10
+ * | `locked` | maintainers only | immediately |
11
+ *
12
+ * The rule is pure and lives here rather than in the API layer for the reason
13
+ * the contract gives for it existing at all: it is enforced SERVER-SIDE, "not
14
+ * merely reflected in the interface". A pure function with an exhaustive
15
+ * decision table is testable against every combination of mode, role, and
16
+ * credential kind without standing up a request, which is the only way to be
17
+ * confident that `locked` really does admit a maintainer's agent and really
18
+ * does refuse an editor.
19
+ *
20
+ * Two invariants hold across every mode and are asserted by the tests:
21
+ *
22
+ * * **Anonymous writing is never available**, including under `open`.
23
+ * Design §19.7 defers it until moderation, spam controls, privacy, and a
24
+ * deletion policy all exist; Phase 7 supplies the first of the four.
25
+ * * **An agent never inherits its owner's access.** An agent token writes
26
+ * only through a membership of its own, which is the ordinary
27
+ * scope-intersection rule of Phase 2 §3 and a deliberate grant. That is
28
+ * also exactly how an author's agent keeps working under `locked`: it
29
+ * holds a membership with the maintainer role, granted on purpose.
30
+ */
31
+ import { type Decision } from "./decision.js";
32
+ import type { Role } from "./scopes.js";
33
+ export declare const ANNOTATION_POLICIES: readonly ["open", "approval-gated", "collaborators-only", "locked"];
34
+ export type AnnotationPolicy = (typeof ANNOTATION_POLICIES)[number];
35
+ /**
36
+ * The mode a book with no declared policy runs in: the Phase 2 behaviour,
37
+ * unchanged. A deployment upgrading into Phase 7 must not find its book
38
+ * suddenly writable by strangers, so the default is the restrictive end of the
39
+ * progression that people were already on — not the permissive end, and not
40
+ * `locked` either, which would lock out the collaborators they already have.
41
+ */
42
+ export declare const DEFAULT_ANNOTATION_POLICY: AnnotationPolicy;
43
+ export declare function isAnnotationPolicy(value: unknown): value is AnnotationPolicy;
44
+ /** True when writes under this policy are queued for review rather than published. */
45
+ export declare function policyRequiresApproval(policy: AnnotationPolicy): boolean;
46
+ export type AnnotationPolicyDenialReason =
47
+ /** No credential at all. Refused in every mode, `open` included. */
48
+ "anonymous"
49
+ /** Signed in, but this mode admits only members of the project. */
50
+ | "members-only"
51
+ /** The book is locked: maintainers only. */
52
+ | "locked"
53
+ /**
54
+ * Signed in and not a member, asking for something a non-member could never
55
+ * do (voting, claiming, submitting) even in an open book — those are member
56
+ * capabilities, and `open` widens commenting, not the work queue.
57
+ */
58
+ | "member-capability";
59
+ /**
60
+ * The write capabilities the policy arbitrates.
61
+ *
62
+ * `annotate` is the one the policy table is actually about — commenting and
63
+ * suggesting. The other three are member capabilities that `open` and
64
+ * `approval-gated` do NOT widen to strangers: a book that welcomes comments
65
+ * from the internet is not thereby handing the internet its governance votes
66
+ * or its work queue.
67
+ *
68
+ * `locked` restricts all four, because "existing collaborators keep their
69
+ * membership and their history — they simply cannot write until the policy
70
+ * opens again", and a vote that manufactures a work item is unambiguously a
71
+ * write.
72
+ */
73
+ export type PolicyCapability = "annotate" | "vote" | "claim" | "submit";
74
+ export interface AnnotationPolicyRequest {
75
+ policy: AnnotationPolicy;
76
+ /** Absent for an unauthenticated request. */
77
+ credential: "session" | "token" | null;
78
+ /** The actor's role through an unrevoked membership; null when not a member. */
79
+ role: Role | null;
80
+ capability: PolicyCapability;
81
+ }
82
+ /**
83
+ * Decide whether the policy admits this write.
84
+ *
85
+ * This answers the policy question ONLY. Scope checks, freeze, the agent pause,
86
+ * and rate limits are separate gates applied alongside it — a request must
87
+ * satisfy all of them, and each refuses with its own problem type so the caller
88
+ * can tell "the book is locked" from "the book is frozen" from "you are going
89
+ * too fast".
90
+ */
91
+ export declare function checkAnnotationPolicy(request: AnnotationPolicyRequest): Decision<AnnotationPolicyDenialReason>;
92
+ //# sourceMappingURL=annotation-policy.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Annotation policy (Phase 7 contract "Restricting") — who may write to a
3
+ * book, and whether what they write appears immediately.
4
+ *
5
+ * | Mode | Who may write | Appears |
6
+ * |---------------------|--------------------------|----------------------------|
7
+ * | `open` | any signed-in user | immediately |
8
+ * | `approval-gated` | any signed-in user | after a maintainer approves|
9
+ * | `collaborators-only`| members only | immediately *(default)* |
10
+ * | `locked` | maintainers only | immediately |
11
+ *
12
+ * The rule is pure and lives here rather than in the API layer for the reason
13
+ * the contract gives for it existing at all: it is enforced SERVER-SIDE, "not
14
+ * merely reflected in the interface". A pure function with an exhaustive
15
+ * decision table is testable against every combination of mode, role, and
16
+ * credential kind without standing up a request, which is the only way to be
17
+ * confident that `locked` really does admit a maintainer's agent and really
18
+ * does refuse an editor.
19
+ *
20
+ * Two invariants hold across every mode and are asserted by the tests:
21
+ *
22
+ * * **Anonymous writing is never available**, including under `open`.
23
+ * Design §19.7 defers it until moderation, spam controls, privacy, and a
24
+ * deletion policy all exist; Phase 7 supplies the first of the four.
25
+ * * **An agent never inherits its owner's access.** An agent token writes
26
+ * only through a membership of its own, which is the ordinary
27
+ * scope-intersection rule of Phase 2 §3 and a deliberate grant. That is
28
+ * also exactly how an author's agent keeps working under `locked`: it
29
+ * holds a membership with the maintainer role, granted on purpose.
30
+ */
31
+ import { ALLOWED, denied } from "./decision.js";
32
+ export const ANNOTATION_POLICIES = [
33
+ "open",
34
+ "approval-gated",
35
+ "collaborators-only",
36
+ "locked",
37
+ ];
38
+ /**
39
+ * The mode a book with no declared policy runs in: the Phase 2 behaviour,
40
+ * unchanged. A deployment upgrading into Phase 7 must not find its book
41
+ * suddenly writable by strangers, so the default is the restrictive end of the
42
+ * progression that people were already on — not the permissive end, and not
43
+ * `locked` either, which would lock out the collaborators they already have.
44
+ */
45
+ export const DEFAULT_ANNOTATION_POLICY = "collaborators-only";
46
+ export function isAnnotationPolicy(value) {
47
+ return typeof value === "string" && ANNOTATION_POLICIES.includes(value);
48
+ }
49
+ /** True when writes under this policy are queued for review rather than published. */
50
+ export function policyRequiresApproval(policy) {
51
+ return policy === "approval-gated";
52
+ }
53
+ /**
54
+ * Decide whether the policy admits this write.
55
+ *
56
+ * This answers the policy question ONLY. Scope checks, freeze, the agent pause,
57
+ * and rate limits are separate gates applied alongside it — a request must
58
+ * satisfy all of them, and each refuses with its own problem type so the caller
59
+ * can tell "the book is locked" from "the book is frozen" from "you are going
60
+ * too fast".
61
+ */
62
+ export function checkAnnotationPolicy(request) {
63
+ const { policy, credential, role, capability } = request;
64
+ // Invariant 1, checked before anything else so no mode can accidentally
65
+ // relax it: anonymous writing is unavailable everywhere.
66
+ if (credential === null) {
67
+ return denied("anonymous", "signing in is required to write to this book; anonymous contributions are not accepted in any mode");
68
+ }
69
+ if (policy === "locked") {
70
+ if (role === "maintainer") {
71
+ return ALLOWED;
72
+ }
73
+ return denied("locked", "this book is locked: only its maintainers may write. Existing collaborators keep their membership and their history, and may write again when the author reopens the policy.");
74
+ }
75
+ const isMember = role !== null;
76
+ if (policy === "open" || policy === "approval-gated") {
77
+ if (isMember) {
78
+ return ALLOWED;
79
+ }
80
+ // Invariant 2: a non-member agent token is NOT admitted by a permissive
81
+ // policy. `open` means "any signed-in GitHub user", which describes a
82
+ // person; an agent reaches a book through a membership granted to it on
83
+ // purpose, never by its owner's access spilling over.
84
+ if (credential === "token") {
85
+ return denied("members-only", "an agent token writes only through its own project membership; an open annotation policy admits signed-in people, not unenrolled agents");
86
+ }
87
+ if (capability !== "annotate") {
88
+ return denied("member-capability", `${capability} is a collaborator capability: an open annotation policy widens commenting and suggesting, not voting, claiming, or submitting`);
89
+ }
90
+ return ALLOWED;
91
+ }
92
+ // collaborators-only — the Phase 2 behaviour, and the default.
93
+ if (isMember) {
94
+ return ALLOWED;
95
+ }
96
+ return denied("members-only", "this book accepts writes from its collaborators only");
97
+ }
98
+ //# sourceMappingURL=annotation-policy.js.map
@@ -0,0 +1,32 @@
1
+ import { ANNOTATION_STATUSES, type AnnotationStatus } from "@authorbot/schemas";
2
+ import { type Decision } from "./decision.js";
3
+ import type { Role } from "./scopes.js";
4
+ export { ANNOTATION_STATUSES };
5
+ export type { AnnotationStatus };
6
+ /**
7
+ * Annotation state machine (design section 9.4; statuses per Phase 0 contract
8
+ * section 4). `open` fans out; `rejected -> open` is the Phase 3 maintainer
9
+ * reopen override (Phase 3 contract section 4); every other status is
10
+ * terminal. The reanchor flow (`needs_reanchor -> open`) is intentionally not
11
+ * legal yet — adding transitions later is additive.
12
+ */
13
+ export declare const ANNOTATION_TRANSITIONS: Readonly<Record<AnnotationStatus, readonly AnnotationStatus[]>>;
14
+ export declare function canTransitionAnnotation(from: AnnotationStatus, to: AnnotationStatus): boolean;
15
+ export type AnnotationTransitionDenialReason = "illegal-transition";
16
+ /** Typed decision for a requested annotation status change. */
17
+ export declare function transitionAnnotation(from: AnnotationStatus, to: AnnotationStatus): Decision<AnnotationTransitionDenialReason>;
18
+ export type WithdrawDenialReason = "not-author-or-maintainer" | "illegal-transition";
19
+ /**
20
+ * Withdraw authorization rule (Phase 2 contract section 4): withdraw is
21
+ * author-or-maintainer, and only an `open` annotation can be withdrawn.
22
+ * `annotationAuthor` and `actor` are canonical actor refs
23
+ * (`<namespace>:<identifier>`, Phase 0 contract section 2); comparison is
24
+ * exact string equality.
25
+ */
26
+ export declare function authorizeAnnotationWithdraw(input: {
27
+ annotationAuthor: string;
28
+ annotationStatus: AnnotationStatus;
29
+ actor: string;
30
+ actorRole: Role;
31
+ }): Decision<WithdrawDenialReason>;
32
+ //# sourceMappingURL=annotation-state.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { ANNOTATION_STATUSES } from "@authorbot/schemas";
2
+ import { ALLOWED, denied } from "./decision.js";
3
+ export { ANNOTATION_STATUSES };
4
+ /**
5
+ * Annotation state machine (design section 9.4; statuses per Phase 0 contract
6
+ * section 4). `open` fans out; `rejected -> open` is the Phase 3 maintainer
7
+ * reopen override (Phase 3 contract section 4); every other status is
8
+ * terminal. The reanchor flow (`needs_reanchor -> open`) is intentionally not
9
+ * legal yet — adding transitions later is additive.
10
+ */
11
+ export const ANNOTATION_TRANSITIONS = Object.freeze({
12
+ open: [
13
+ "work_item_created",
14
+ "accepted",
15
+ "resolved",
16
+ "rejected",
17
+ "withdrawn",
18
+ "superseded",
19
+ "orphaned",
20
+ "needs_reanchor",
21
+ ],
22
+ work_item_created: [],
23
+ accepted: [],
24
+ resolved: [],
25
+ rejected: ["open"],
26
+ withdrawn: [],
27
+ superseded: [],
28
+ orphaned: [],
29
+ needs_reanchor: [],
30
+ });
31
+ export function canTransitionAnnotation(from, to) {
32
+ return ANNOTATION_TRANSITIONS[from].includes(to);
33
+ }
34
+ /** Typed decision for a requested annotation status change. */
35
+ export function transitionAnnotation(from, to) {
36
+ if (canTransitionAnnotation(from, to)) {
37
+ return ALLOWED;
38
+ }
39
+ return denied("illegal-transition", `annotation status cannot change from "${from}" to "${to}"`);
40
+ }
41
+ /**
42
+ * Withdraw authorization rule (Phase 2 contract section 4): withdraw is
43
+ * author-or-maintainer, and only an `open` annotation can be withdrawn.
44
+ * `annotationAuthor` and `actor` are canonical actor refs
45
+ * (`<namespace>:<identifier>`, Phase 0 contract section 2); comparison is
46
+ * exact string equality.
47
+ */
48
+ export function authorizeAnnotationWithdraw(input) {
49
+ if (input.actor !== input.annotationAuthor && input.actorRole !== "maintainer") {
50
+ return denied("not-author-or-maintainer", "only the annotation author or a maintainer may withdraw an annotation");
51
+ }
52
+ const transition = transitionAnnotation(input.annotationStatus, "withdrawn");
53
+ if (!transition.allowed) {
54
+ return denied("illegal-transition", `annotation with status "${input.annotationStatus}" cannot be withdrawn`);
55
+ }
56
+ return ALLOWED;
57
+ }
58
+ //# sourceMappingURL=annotation-state.js.map
@@ -0,0 +1,129 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Command validators (Phase 2 contract section 4). A command is the API
4
+ * payload plus its route parameters merged by the API layer (e.g. the
5
+ * `chapterId` from the URL joins the annotation payload), so validation of a
6
+ * whole logical command lives in one place. Markdown safety of bodies (raw
7
+ * HTML, URL schemes) stays with `@authorbot/markdown` at the API layer; this
8
+ * package enforces shape, sizes, and cross-field rules only.
9
+ */
10
+ /** UTF-8 byte length without relying on host globals (pure, worker-safe). */
11
+ export declare function utf8ByteLength(value: string): number;
12
+ /** Body limit (contract section 4): Markdown <= 32 KiB, measured in UTF-8 bytes. */
13
+ export declare const MAX_BODY_BYTES: number;
14
+ /**
15
+ * Canonical body form: CRLF folded to LF, leading/trailing whitespace
16
+ * trimmed. This is exactly the normalization the artifact renderer applies
17
+ * (repo-coordinator render.ts) and the repo reader re-applies on read, so
18
+ * normalizing once at intake keeps the DB row, the committed artifact, and a
19
+ * rebuilt projection byte-identical (a projection rebuild must not change
20
+ * annotation/reply bodies served by the API).
21
+ */
22
+ export declare function normalizeBody(value: string): string;
23
+ export declare const bodySchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
24
+ /** Range target with the contract's ordering rule: `textPosition.end > start`. */
25
+ export declare const orderedRangeTargetSchema: z.ZodObject<{
26
+ blockId: z.ZodString;
27
+ textPosition: z.ZodObject<{
28
+ start: z.ZodNumber;
29
+ end: z.ZodNumber;
30
+ }, z.core.$strict>;
31
+ textQuote: z.ZodObject<{
32
+ exact: z.ZodString;
33
+ prefix: z.ZodOptional<z.ZodString>;
34
+ suffix: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strict>;
36
+ }, z.core.$strict>;
37
+ /**
38
+ * `POST .../chapters/{chapterId}/annotations` (contract section 4).
39
+ * `target` is required for `range` (blockId + textPosition + textQuote) and
40
+ * `block` (blockId only) scopes and forbidden for `chapter` scope. Whether
41
+ * the chapter/revision/block actually exist is the API's projection check.
42
+ */
43
+ export declare const createAnnotationCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
+ scope: z.ZodLiteral<"range">;
45
+ target: z.ZodObject<{
46
+ blockId: z.ZodString;
47
+ textPosition: z.ZodObject<{
48
+ start: z.ZodNumber;
49
+ end: z.ZodNumber;
50
+ }, z.core.$strict>;
51
+ textQuote: z.ZodObject<{
52
+ exact: z.ZodString;
53
+ prefix: z.ZodOptional<z.ZodString>;
54
+ suffix: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strict>;
56
+ }, z.core.$strict>;
57
+ chapterId: z.ZodString;
58
+ kind: z.ZodEnum<{
59
+ comment: "comment";
60
+ suggestion: "suggestion";
61
+ }>;
62
+ chapterRevision: z.ZodNumber;
63
+ body: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
64
+ }, z.core.$strict>, z.ZodObject<{
65
+ scope: z.ZodLiteral<"block">;
66
+ target: z.ZodObject<{
67
+ blockId: z.ZodString;
68
+ }, z.core.$strict>;
69
+ chapterId: z.ZodString;
70
+ kind: z.ZodEnum<{
71
+ comment: "comment";
72
+ suggestion: "suggestion";
73
+ }>;
74
+ chapterRevision: z.ZodNumber;
75
+ body: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
76
+ }, z.core.$strict>, z.ZodObject<{
77
+ scope: z.ZodLiteral<"chapter">;
78
+ chapterId: z.ZodString;
79
+ kind: z.ZodEnum<{
80
+ comment: "comment";
81
+ suggestion: "suggestion";
82
+ }>;
83
+ chapterRevision: z.ZodNumber;
84
+ body: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
85
+ }, z.core.$strict>], "scope">;
86
+ export type CreateAnnotationCommand = z.infer<typeof createAnnotationCommandSchema>;
87
+ /**
88
+ * `POST .../annotations/{annotationId}/replies`. The contract pins no reply
89
+ * payload; this package pins `{ body, parentReplyId? }` with the same 32 KiB
90
+ * body rule as annotations.
91
+ */
92
+ export declare const createReplyCommandSchema: z.ZodObject<{
93
+ annotationId: z.ZodString;
94
+ parentReplyId: z.ZodOptional<z.ZodString>;
95
+ body: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
96
+ }, z.core.$strict>;
97
+ export type CreateReplyCommand = z.infer<typeof createReplyCommandSchema>;
98
+ /** Token display-name bound (not contract-pinned; chosen for UI sanity). */
99
+ export declare const MAX_TOKEN_NAME_LENGTH = 100;
100
+ /**
101
+ * `POST .../agent-tokens` (contract section 3): name, scopes ⊆ known scopes
102
+ * (non-empty, no duplicates), expiry <= 90 days (default 30).
103
+ */
104
+ export declare const mintAgentTokenCommandSchema: z.ZodObject<{
105
+ name: z.ZodString;
106
+ scopes: z.ZodArray<z.ZodEnum<{
107
+ "chapters:read": "chapters:read";
108
+ "annotations:read": "annotations:read";
109
+ "annotations:write": "annotations:write";
110
+ "work:read": "work:read";
111
+ "work:claim": "work:claim";
112
+ "submissions:write": "submissions:write";
113
+ "tokens:manage": "tokens:manage";
114
+ "members:manage": "members:manage";
115
+ }>>;
116
+ expiresInDays: z.ZodDefault<z.ZodNumber>;
117
+ }, z.core.$strict>;
118
+ export type MintAgentTokenCommand = z.infer<typeof mintAgentTokenCommandSchema>;
119
+ export type MintAgentTokenCommandInput = z.input<typeof mintAgentTokenCommandSchema>;
120
+ /**
121
+ * `POST .../annotations/{annotationId}/withdraw`. The HTTP body is empty;
122
+ * the command is the route parameter. Authorization (author-or-maintainer)
123
+ * is `authorizeAnnotationWithdraw` in annotation-state.
124
+ */
125
+ export declare const withdrawAnnotationCommandSchema: z.ZodObject<{
126
+ annotationId: z.ZodString;
127
+ }, z.core.$strict>;
128
+ export type WithdrawAnnotationCommand = z.infer<typeof withdrawAnnotationCommandSchema>;
129
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+ import { annotationKindSchema, blockTargetSchema, rangeTargetSchema, uuidv7Schema, } from "@authorbot/schemas";
3
+ import { scopeSchema } from "./scopes.js";
4
+ /**
5
+ * Command validators (Phase 2 contract section 4). A command is the API
6
+ * payload plus its route parameters merged by the API layer (e.g. the
7
+ * `chapterId` from the URL joins the annotation payload), so validation of a
8
+ * whole logical command lives in one place. Markdown safety of bodies (raw
9
+ * HTML, URL schemes) stays with `@authorbot/markdown` at the API layer; this
10
+ * package enforces shape, sizes, and cross-field rules only.
11
+ */
12
+ /** UTF-8 byte length without relying on host globals (pure, worker-safe). */
13
+ export function utf8ByteLength(value) {
14
+ let bytes = 0;
15
+ for (const char of value) {
16
+ const code = char.codePointAt(0);
17
+ bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
18
+ }
19
+ return bytes;
20
+ }
21
+ /** Body limit (contract section 4): Markdown <= 32 KiB, measured in UTF-8 bytes. */
22
+ export const MAX_BODY_BYTES = 32 * 1024;
23
+ /**
24
+ * Canonical body form: CRLF folded to LF, leading/trailing whitespace
25
+ * trimmed. This is exactly the normalization the artifact renderer applies
26
+ * (repo-coordinator render.ts) and the repo reader re-applies on read, so
27
+ * normalizing once at intake keeps the DB row, the committed artifact, and a
28
+ * rebuilt projection byte-identical (a projection rebuild must not change
29
+ * annotation/reply bodies served by the API).
30
+ */
31
+ export function normalizeBody(value) {
32
+ return value.replace(/\r\n/g, "\n").trim();
33
+ }
34
+ export const bodySchema = z
35
+ .string()
36
+ .transform(normalizeBody)
37
+ .refine((value) => value.length > 0, "body must not be empty")
38
+ .refine((value) => utf8ByteLength(value) <= MAX_BODY_BYTES, `body must be at most ${MAX_BODY_BYTES} bytes of UTF-8`);
39
+ /** Range target with the contract's ordering rule: `textPosition.end > start`. */
40
+ export const orderedRangeTargetSchema = rangeTargetSchema.refine((target) => target.textPosition.end > target.textPosition.start, { path: ["textPosition", "end"], message: "textPosition.end must be greater than textPosition.start" });
41
+ const createAnnotationBase = {
42
+ chapterId: uuidv7Schema,
43
+ kind: annotationKindSchema,
44
+ chapterRevision: z.number().int().min(1),
45
+ body: bodySchema,
46
+ };
47
+ /**
48
+ * `POST .../chapters/{chapterId}/annotations` (contract section 4).
49
+ * `target` is required for `range` (blockId + textPosition + textQuote) and
50
+ * `block` (blockId only) scopes and forbidden for `chapter` scope. Whether
51
+ * the chapter/revision/block actually exist is the API's projection check.
52
+ */
53
+ export const createAnnotationCommandSchema = z.discriminatedUnion("scope", [
54
+ z.strictObject({
55
+ ...createAnnotationBase,
56
+ scope: z.literal("range"),
57
+ target: orderedRangeTargetSchema,
58
+ }),
59
+ z.strictObject({
60
+ ...createAnnotationBase,
61
+ scope: z.literal("block"),
62
+ target: blockTargetSchema,
63
+ }),
64
+ z.strictObject({
65
+ ...createAnnotationBase,
66
+ scope: z.literal("chapter"),
67
+ }),
68
+ ]);
69
+ /**
70
+ * `POST .../annotations/{annotationId}/replies`. The contract pins no reply
71
+ * payload; this package pins `{ body, parentReplyId? }` with the same 32 KiB
72
+ * body rule as annotations.
73
+ */
74
+ export const createReplyCommandSchema = z.strictObject({
75
+ annotationId: uuidv7Schema,
76
+ parentReplyId: uuidv7Schema.optional(),
77
+ body: bodySchema,
78
+ });
79
+ /** Token display-name bound (not contract-pinned; chosen for UI sanity). */
80
+ export const MAX_TOKEN_NAME_LENGTH = 100;
81
+ /**
82
+ * `POST .../agent-tokens` (contract section 3): name, scopes ⊆ known scopes
83
+ * (non-empty, no duplicates), expiry <= 90 days (default 30).
84
+ */
85
+ export const mintAgentTokenCommandSchema = z.strictObject({
86
+ name: z
87
+ .string()
88
+ .min(1, "name must not be empty")
89
+ .max(MAX_TOKEN_NAME_LENGTH, `name must be at most ${MAX_TOKEN_NAME_LENGTH} characters`),
90
+ scopes: z
91
+ .array(scopeSchema)
92
+ .min(1, "at least one scope is required")
93
+ .refine((scopes) => new Set(scopes).size === scopes.length, "scopes must not contain duplicates"),
94
+ expiresInDays: z.number().int().min(1).max(90).default(30),
95
+ });
96
+ /**
97
+ * `POST .../annotations/{annotationId}/withdraw`. The HTTP body is empty;
98
+ * the command is the route parameter. Authorization (author-or-maintainer)
99
+ * is `authorizeAnnotationWithdraw` in annotation-state.
100
+ */
101
+ export const withdrawAnnotationCommandSchema = z.strictObject({
102
+ annotationId: uuidv7Schema,
103
+ });
104
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1,33 @@
1
+ export { DECISION_RESULTS, decisionResultSchema } from "@authorbot/schemas";
2
+ export type { DecisionResult } from "@authorbot/schemas";
3
+ /**
4
+ * Sticky decision `support_changed` tracking (Phase 3 contract section 4,
5
+ * design section 11.3). Later vote changes never delete a decision or its
6
+ * work item; the decision only gains/loses a `support_changed` mark as the
7
+ * live aggregate stops/starts satisfying the rule, with an event on each
8
+ * flip. The original threshold-crossing metric snapshot is preserved
9
+ * elsewhere (the decision row/artifact) and never rewritten here.
10
+ */
11
+ /** Event emitted whenever the mark flips (contract section 5). */
12
+ export declare const DECISION_SUPPORT_CHANGED_EVENT: "decision_support_changed";
13
+ export type SupportChangeTransition = "marked" | "cleared" | "unchanged";
14
+ export interface SupportChangeOutcome {
15
+ /** The mark after applying the rule outcome. */
16
+ readonly supportChanged: boolean;
17
+ readonly transition: SupportChangeTransition;
18
+ /** True exactly when a `decision_support_changed` event must be emitted. */
19
+ readonly emitEvent: boolean;
20
+ }
21
+ /**
22
+ * Pure transition: given the decision's current mark and whether the live
23
+ * aggregate still satisfies the rule, decide the new mark.
24
+ *
25
+ * - satisfied + marked → `cleared` (support returned)
26
+ * - unsatisfied + unmarked → `marked` (support fell away)
27
+ * - otherwise → `unchanged`, no event
28
+ */
29
+ export declare function resolveSupportChange(input: {
30
+ supportChanged: boolean;
31
+ ruleSatisfied: boolean;
32
+ }): SupportChangeOutcome;
33
+ //# sourceMappingURL=decision-support.d.ts.map
@@ -0,0 +1,29 @@
1
+ export { DECISION_RESULTS, decisionResultSchema } from "@authorbot/schemas";
2
+ /**
3
+ * Sticky decision `support_changed` tracking (Phase 3 contract section 4,
4
+ * design section 11.3). Later vote changes never delete a decision or its
5
+ * work item; the decision only gains/loses a `support_changed` mark as the
6
+ * live aggregate stops/starts satisfying the rule, with an event on each
7
+ * flip. The original threshold-crossing metric snapshot is preserved
8
+ * elsewhere (the decision row/artifact) and never rewritten here.
9
+ */
10
+ /** Event emitted whenever the mark flips (contract section 5). */
11
+ export const DECISION_SUPPORT_CHANGED_EVENT = "decision_support_changed";
12
+ /**
13
+ * Pure transition: given the decision's current mark and whether the live
14
+ * aggregate still satisfies the rule, decide the new mark.
15
+ *
16
+ * - satisfied + marked → `cleared` (support returned)
17
+ * - unsatisfied + unmarked → `marked` (support fell away)
18
+ * - otherwise → `unchanged`, no event
19
+ */
20
+ export function resolveSupportChange(input) {
21
+ if (input.ruleSatisfied && input.supportChanged) {
22
+ return { supportChanged: false, transition: "cleared", emitEvent: true };
23
+ }
24
+ if (!input.ruleSatisfied && !input.supportChanged) {
25
+ return { supportChanged: true, transition: "marked", emitEvent: true };
26
+ }
27
+ return { supportChanged: input.supportChanged, transition: "unchanged", emitEvent: false };
28
+ }
29
+ //# sourceMappingURL=decision-support.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Typed authorization/transition decisions (Phase 2 contract section 3).
3
+ * Every domain rule that can refuse returns `allowed | denied(reason)` so the
4
+ * API layer can map reasons to problem+json types without string matching.
5
+ */
6
+ export interface Allowed {
7
+ readonly allowed: true;
8
+ }
9
+ export interface Denied<TReason extends string = string> {
10
+ readonly allowed: false;
11
+ readonly reason: TReason;
12
+ /** Human-readable explanation; safe to surface (never contains secrets). */
13
+ readonly message: string;
14
+ }
15
+ export type Decision<TReason extends string = string> = Allowed | Denied<TReason>;
16
+ export declare const ALLOWED: Allowed;
17
+ export declare function denied<TReason extends string>(reason: TReason, message: string): Denied<TReason>;
18
+ //# sourceMappingURL=decision.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Typed authorization/transition decisions (Phase 2 contract section 3).
3
+ * Every domain rule that can refuse returns `allowed | denied(reason)` so the
4
+ * API layer can map reasons to problem+json types without string matching.
5
+ */
6
+ export const ALLOWED = Object.freeze({ allowed: true });
7
+ export function denied(reason, message) {
8
+ return { allowed: false, reason, message };
9
+ }
10
+ //# sourceMappingURL=decision.js.map