@authorbot/domain 0.1.33 → 0.1.35

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
@@ -9,6 +9,72 @@ Every published package shares this version. A tag builds, tests, and publishes
9
9
  all of them together, so `@authorbot/cli@0.1.15` and `@authorbot/api@0.1.15` are
10
10
  always the same commit.
11
11
 
12
+ ## 0.1.35
13
+
14
+ - **Legacy agent-token requests now maintain the canonical projection.** The
15
+ compatibility `{scopes}` request remains legacy-authoritative, but every new
16
+ token also records the exact safe capability translation. Control-plane
17
+ scopes never enter that projection. This dual-write Worker must be deployed
18
+ and verified before the one-shot legacy-row backfill ships in v0.1.36.
19
+ - **The upgrade helper now repairs a stale local toolchain before changing a
20
+ book.** Forward upgrades hand off to the exact target CLI in a throwaway
21
+ install when `node_modules` does not match the repository pin. That target
22
+ aligns the CLI and API packages, regenerates the lockfile, and preserves the
23
+ child exit code and JSON output. Interrupted same-version installs are
24
+ repaired through a normal reviewable pull request, with migration baselines
25
+ accepted only from coherent committed CLI lock evidence. Dry runs regenerate
26
+ and verify the lockfile in a throwaway copy. Windows launches npm, npx, and
27
+ Wrangler through validated JavaScript entry points under the current Node
28
+ executable, without enabling a command shell. Exact `--to` targets require
29
+ no registry metadata lookup; implicit discovery uses npm's configured
30
+ offline cache, registry, userconfig, and authentication. Existing helpers
31
+ from before 0.1.35 still need one explicit
32
+ `npx --yes @authorbot/cli@0.1.35 upgrade --to 0.1.35` launch; ordinary
33
+ `npx authorbot upgrade` self-bootstraps after that.
34
+ - This is a focused capability-writer-gate and upgrade-safety release. It adds
35
+ no D1 or book-format migration. The Phase 3B backfill is reserved for
36
+ v0.1.36 after this Worker is verified; Phase 3C legacy retirement remains a
37
+ deliberately separate later release.
38
+
39
+ ## 0.1.34
40
+
41
+ - **The full editorial workflow is now available from the site.** Editors can
42
+ revise chapters with Milkdown Crepe, edit Outline, Timeline, and Character
43
+ documents in place, submit immutable proposals, and review safe Diff2Html
44
+ diffs. Maintainers can apply their own edit in one action without bypassing
45
+ validation, attribution, Git history, or the deployment pipeline.
46
+ - **Notes and discussion now work as complete chapter collaboration tools.**
47
+ Anchored notes stay in manuscript order, expand with their target, navigate
48
+ with Previous and Next controls, render inline on mobile, and share the
49
+ Milkdown reading surface. Chapter-wide discussion supports replies, comment
50
+ voting, withdrawal, moderation, and one-click promotion to Work.
51
+ - **Work and revision progress remains visible.** Completed Work stays on the
52
+ Work page with attribution and result links. Submitted edits report review,
53
+ apply, repository, build, and deployment state while retaining recoverable
54
+ drafts until the matching commit is live.
55
+ - **Agent permissions are exact and human-equivalent for editorial work.**
56
+ Tokens can independently read and write comments and suggestions, vote,
57
+ reply, work a lease, submit revisions, review proposals, read history, and
58
+ update summaries within their role ceiling. Control-plane authority remains
59
+ human-only. Token event feeds and operation polling now enforce those same
60
+ exact read capabilities instead of exposing unrelated project activity. The
61
+ collaborator skill documents these workflows and exposes a bounded
62
+ story-bible API so agents do not have to probe a deployed site.
63
+ - **Editors load reliably on nested and mobile sites.** Books served from a
64
+ base path keep lazy editor styles under that path. Mobile note forms stay
65
+ reachable beside their manuscript target, and rich Notes rerenders replace
66
+ the existing composer instead of duplicating it.
67
+ - **Chapter context is easier to understand and recover.** Chapter rows show
68
+ open feedback and Work counts, summaries flow into the authenticated and
69
+ published Outline views, contributors link to the accepted revisions that
70
+ credited them, and authorized collaborators can browse, compare, and propose
71
+ restoring earlier chapter versions.
72
+ - This release includes D1 migrations `0010_phase11_capabilities_expand.sql`,
73
+ `0011_phase11_revision_proposals.sql`, and
74
+ `0012_chapter_summaries.sql`. The capability backfill deliberately remains a
75
+ separate later release after this dual-read Worker is deployed. No
76
+ book-format migration is required.
77
+
12
78
  ## 0.1.33
13
79
 
14
80
  - **Collaboration state now stays coherent across the whole site.** Account,
@@ -126,4 +126,14 @@ export declare const withdrawAnnotationCommandSchema: z.ZodObject<{
126
126
  annotationId: z.ZodString;
127
127
  }, z.core.$strict>;
128
128
  export type WithdrawAnnotationCommand = z.infer<typeof withdrawAnnotationCommandSchema>;
129
+ /**
130
+ * `POST .../annotations/{annotationId}/replies/{replyId}/withdraw`. The HTTP
131
+ * body is empty; both route parameters are part of the logical command so an
132
+ * id cannot be moved across annotation threads by changing only the URL.
133
+ */
134
+ export declare const withdrawReplyCommandSchema: z.ZodObject<{
135
+ annotationId: z.ZodString;
136
+ replyId: z.ZodString;
137
+ }, z.core.$strict>;
138
+ export type WithdrawReplyCommand = z.infer<typeof withdrawReplyCommandSchema>;
129
139
  //# sourceMappingURL=commands.d.ts.map
package/dist/commands.js CHANGED
@@ -101,4 +101,13 @@ export const mintAgentTokenCommandSchema = z.strictObject({
101
101
  export const withdrawAnnotationCommandSchema = z.strictObject({
102
102
  annotationId: uuidv7Schema,
103
103
  });
104
+ /**
105
+ * `POST .../annotations/{annotationId}/replies/{replyId}/withdraw`. The HTTP
106
+ * body is empty; both route parameters are part of the logical command so an
107
+ * id cannot be moved across annotation threads by changing only the URL.
108
+ */
109
+ export const withdrawReplyCommandSchema = z.strictObject({
110
+ annotationId: uuidv7Schema,
111
+ replyId: uuidv7Schema,
112
+ });
104
113
  //# sourceMappingURL=commands.js.map
@@ -0,0 +1,158 @@
1
+ import { z } from "zod";
2
+ import { type Decision } from "./decision.js";
3
+ import type { Role } from "./scopes.js";
4
+ /**
5
+ * Canonical Phase 11 editorial authority for agent tokens.
6
+ *
7
+ * This vocabulary is deliberately exact. A capability added by a later
8
+ * release is absent from this tuple and therefore fails parsing instead of
9
+ * inheriting authority from a broad read or write grant.
10
+ */
11
+ export declare const EDITORIAL_CAPABILITIES: readonly ["chapters:read", "comments:read", "suggestions:read", "comments:write", "suggestions:write", "replies:write", "comments:vote", "suggestions:vote", "feedback:withdraw-own", "feedback:moderate", "work:read", "work:promote", "work:claim", "work:submit", "work:cancel", "summaries:write", "chapters:write", "chapters:publish", "revisions:read", "revisions:write", "revisions:review", "history:read"];
12
+ export type EditorialCapability = (typeof EDITORIAL_CAPABILITIES)[number];
13
+ export declare const editorialCapabilitySchema: z.ZodEnum<{
14
+ "chapters:read": "chapters:read";
15
+ "work:read": "work:read";
16
+ "work:claim": "work:claim";
17
+ "comments:read": "comments:read";
18
+ "suggestions:read": "suggestions:read";
19
+ "comments:write": "comments:write";
20
+ "suggestions:write": "suggestions:write";
21
+ "replies:write": "replies:write";
22
+ "comments:vote": "comments:vote";
23
+ "suggestions:vote": "suggestions:vote";
24
+ "feedback:withdraw-own": "feedback:withdraw-own";
25
+ "feedback:moderate": "feedback:moderate";
26
+ "work:promote": "work:promote";
27
+ "work:submit": "work:submit";
28
+ "work:cancel": "work:cancel";
29
+ "summaries:write": "summaries:write";
30
+ "chapters:write": "chapters:write";
31
+ "chapters:publish": "chapters:publish";
32
+ "revisions:read": "revisions:read";
33
+ "revisions:write": "revisions:write";
34
+ "revisions:review": "revisions:review";
35
+ "history:read": "history:read";
36
+ }>;
37
+ /**
38
+ * A complete canonical grant set as stored on a canonical-mode token.
39
+ *
40
+ * Empty is valid and means that the token has no editorial authority. Unknown
41
+ * and duplicate names invalidate the complete set rather than being partially
42
+ * accepted. The transform gives every valid result one deterministic order.
43
+ */
44
+ export declare const editorialCapabilitiesSchema: z.ZodPipe<z.ZodArray<z.ZodEnum<{
45
+ "chapters:read": "chapters:read";
46
+ "work:read": "work:read";
47
+ "work:claim": "work:claim";
48
+ "comments:read": "comments:read";
49
+ "suggestions:read": "suggestions:read";
50
+ "comments:write": "comments:write";
51
+ "suggestions:write": "suggestions:write";
52
+ "replies:write": "replies:write";
53
+ "comments:vote": "comments:vote";
54
+ "suggestions:vote": "suggestions:vote";
55
+ "feedback:withdraw-own": "feedback:withdraw-own";
56
+ "feedback:moderate": "feedback:moderate";
57
+ "work:promote": "work:promote";
58
+ "work:submit": "work:submit";
59
+ "work:cancel": "work:cancel";
60
+ "summaries:write": "summaries:write";
61
+ "chapters:write": "chapters:write";
62
+ "chapters:publish": "chapters:publish";
63
+ "revisions:read": "revisions:read";
64
+ "revisions:write": "revisions:write";
65
+ "revisions:review": "revisions:review";
66
+ "history:read": "history:read";
67
+ }>>, z.ZodTransform<("chapters:read" | "work:read" | "work:claim" | "comments:read" | "suggestions:read" | "comments:write" | "suggestions:write" | "replies:write" | "comments:vote" | "suggestions:vote" | "feedback:withdraw-own" | "feedback:moderate" | "work:promote" | "work:submit" | "work:cancel" | "summaries:write" | "chapters:write" | "chapters:publish" | "revisions:read" | "revisions:write" | "revisions:review" | "history:read")[], ("chapters:read" | "work:read" | "work:claim" | "comments:read" | "suggestions:read" | "comments:write" | "suggestions:write" | "replies:write" | "comments:vote" | "suggestions:vote" | "feedback:withdraw-own" | "feedback:moderate" | "work:promote" | "work:submit" | "work:cancel" | "summaries:write" | "chapters:write" | "chapters:publish" | "revisions:read" | "revisions:write" | "revisions:review" | "history:read")[]>>;
68
+ export type EditorialCapabilityParseFailureReason = "invalid-capability-set" | "invalid-json";
69
+ export type EditorialCapabilityParseResult = {
70
+ readonly ok: true;
71
+ readonly capabilities: EditorialCapability[];
72
+ } | {
73
+ readonly ok: false;
74
+ readonly capabilities: readonly [];
75
+ readonly reason: EditorialCapabilityParseFailureReason;
76
+ };
77
+ /** Parse a decoded canonical capability value, denying every grant on error. */
78
+ export declare function parseEditorialCapabilities(input: unknown): EditorialCapabilityParseResult;
79
+ /** Parse the JSON stored in `capabilities_v2`, denying every grant on error. */
80
+ export declare function parseStoredEditorialCapabilities(input: string | null): EditorialCapabilityParseResult;
81
+ /**
82
+ * Maximum canonical authority admitted by each current project role.
83
+ * Token grants and this ceiling are independent; effective authority is their
84
+ * intersection.
85
+ */
86
+ export declare const ROLE_EDITORIAL_CAPABILITIES: Readonly<Record<Role, readonly EditorialCapability[]>>;
87
+ export declare function roleEditorialCapabilities(role: Role): readonly EditorialCapability[];
88
+ /** Token grants intersected with the actor's current role, in canonical order. */
89
+ export declare function effectiveEditorialCapabilities(grantedCapabilities: readonly EditorialCapability[], role: Role): EditorialCapability[];
90
+ export type EditorialCapabilityDenialReason = "missing-capability" | "role-ceiling";
91
+ /** Enforce the independent exact-grant and current-role checks. */
92
+ export declare function authorizeEditorialCapability(grantedCapabilities: readonly EditorialCapability[], role: Role, required: EditorialCapability): Decision<EditorialCapabilityDenialReason>;
93
+ /** Legacy names that exercised editorial authority before canonical mode. */
94
+ export declare const LEGACY_EDITORIAL_SCOPES: readonly ["chapters:read", "annotations:read", "annotations:write", "work:read", "work:claim", "submissions:write", "votes:write"];
95
+ export type LegacyEditorialScope = (typeof LEGACY_EDITORIAL_SCOPES)[number];
96
+ /** Known old control-plane names. They never translate to editorial authority. */
97
+ export declare const LEGACY_CONTROL_SCOPES: readonly ["tokens:manage", "members:manage"];
98
+ export type LegacyControlScope = (typeof LEGACY_CONTROL_SCOPES)[number];
99
+ export declare const LEGACY_AGENT_SCOPES: readonly ["chapters:read", "annotations:read", "annotations:write", "work:read", "work:claim", "submissions:write", "tokens:manage", "members:manage", "votes:write"];
100
+ export type LegacyAgentScope = (typeof LEGACY_AGENT_SCOPES)[number];
101
+ export declare const legacyAgentScopeSchema: z.ZodEnum<{
102
+ "chapters:read": "chapters:read";
103
+ "annotations:read": "annotations:read";
104
+ "annotations:write": "annotations:write";
105
+ "work:read": "work:read";
106
+ "work:claim": "work:claim";
107
+ "submissions:write": "submissions:write";
108
+ "tokens:manage": "tokens:manage";
109
+ "members:manage": "members:manage";
110
+ "votes:write": "votes:write";
111
+ }>;
112
+ /**
113
+ * Translate safe, known legacy editorial names. Unknown and control-plane
114
+ * names never produce authority, and prerequisites are deliberately not
115
+ * synthesized.
116
+ */
117
+ export declare function translateLegacyScopes(scopes: readonly string[]): EditorialCapability[];
118
+ export type LegacyScopeParseResult = {
119
+ readonly ok: true;
120
+ readonly scopes: LegacyEditorialScope[];
121
+ readonly removedScopes: string[];
122
+ readonly capabilities: EditorialCapability[];
123
+ } | {
124
+ readonly ok: false;
125
+ readonly scopes: readonly [];
126
+ readonly removedScopes: readonly [];
127
+ readonly capabilities: readonly [];
128
+ readonly reason: "invalid-legacy-scope-set";
129
+ };
130
+ /**
131
+ * Sanitize a decoded legacy scope set for dual-read mode.
132
+ *
133
+ * Known control-plane and unknown string names are returned for audit and
134
+ * removed. Structurally malformed input denies the complete set so a corrupt
135
+ * row cannot partially authorize a request.
136
+ */
137
+ export declare function parseLegacyScopes(input: unknown): LegacyScopeParseResult;
138
+ export declare const LEGACY_COMPATIBILITY_ACTIONS: readonly ["feedback:moderate", "work:promote", "work:cancel"];
139
+ export type LegacyCompatibilityAction = (typeof LEGACY_COMPATIBILITY_ACTIONS)[number];
140
+ export interface LegacyEffectiveAction {
141
+ readonly action: LegacyCompatibilityAction;
142
+ readonly source: "legacy-scope";
143
+ readonly sourceScope: "annotations:write" | "work:claim";
144
+ }
145
+ /**
146
+ * Old maintainer-only actions preserved only while a row remains in legacy
147
+ * mode. They are reported separately and are never canonical grants.
148
+ */
149
+ export declare function legacyEffectiveActions(scopes: readonly string[], role: Role): LegacyEffectiveAction[];
150
+ /**
151
+ * Conservative old-scope shadow for a canonical row.
152
+ *
153
+ * A prior Worker may deny a granular action after deploy or rollback, but
154
+ * every scope it does see is guaranteed not to exceed the canonical grant.
155
+ * Control-plane and unknown names can never be emitted.
156
+ */
157
+ export declare function legacyScopeShadow(capabilities: readonly EditorialCapability[]): LegacyEditorialScope[];
158
+ //# sourceMappingURL=editorial-capabilities.d.ts.map
@@ -0,0 +1,297 @@
1
+ import { z } from "zod";
2
+ import { ALLOWED, denied } from "./decision.js";
3
+ /**
4
+ * Canonical Phase 11 editorial authority for agent tokens.
5
+ *
6
+ * This vocabulary is deliberately exact. A capability added by a later
7
+ * release is absent from this tuple and therefore fails parsing instead of
8
+ * inheriting authority from a broad read or write grant.
9
+ */
10
+ export const EDITORIAL_CAPABILITIES = [
11
+ "chapters:read",
12
+ "comments:read",
13
+ "suggestions:read",
14
+ "comments:write",
15
+ "suggestions:write",
16
+ "replies:write",
17
+ "comments:vote",
18
+ "suggestions:vote",
19
+ "feedback:withdraw-own",
20
+ "feedback:moderate",
21
+ "work:read",
22
+ "work:promote",
23
+ "work:claim",
24
+ "work:submit",
25
+ "work:cancel",
26
+ "summaries:write",
27
+ "chapters:write",
28
+ "chapters:publish",
29
+ "revisions:read",
30
+ "revisions:write",
31
+ "revisions:review",
32
+ "history:read",
33
+ ];
34
+ export const editorialCapabilitySchema = z.enum(EDITORIAL_CAPABILITIES);
35
+ /**
36
+ * A complete canonical grant set as stored on a canonical-mode token.
37
+ *
38
+ * Empty is valid and means that the token has no editorial authority. Unknown
39
+ * and duplicate names invalidate the complete set rather than being partially
40
+ * accepted. The transform gives every valid result one deterministic order.
41
+ */
42
+ export const editorialCapabilitiesSchema = z
43
+ .array(editorialCapabilitySchema)
44
+ .superRefine((capabilities, ctx) => {
45
+ const seen = new Set();
46
+ capabilities.forEach((capability, index) => {
47
+ if (seen.has(capability)) {
48
+ ctx.addIssue({
49
+ code: "custom",
50
+ message: `duplicate capability "${capability}"`,
51
+ path: [index],
52
+ });
53
+ }
54
+ seen.add(capability);
55
+ });
56
+ })
57
+ .transform((capabilities) => {
58
+ const granted = new Set(capabilities);
59
+ return EDITORIAL_CAPABILITIES.filter((capability) => granted.has(capability));
60
+ });
61
+ const invalidCapabilitySet = (reason) => ({
62
+ ok: false,
63
+ capabilities: [],
64
+ reason,
65
+ });
66
+ /** Parse a decoded canonical capability value, denying every grant on error. */
67
+ export function parseEditorialCapabilities(input) {
68
+ const parsed = editorialCapabilitiesSchema.safeParse(input);
69
+ return parsed.success
70
+ ? { ok: true, capabilities: parsed.data }
71
+ : invalidCapabilitySet("invalid-capability-set");
72
+ }
73
+ /** Parse the JSON stored in `capabilities_v2`, denying every grant on error. */
74
+ export function parseStoredEditorialCapabilities(input) {
75
+ if (input === null) {
76
+ return invalidCapabilitySet("invalid-capability-set");
77
+ }
78
+ let decoded;
79
+ try {
80
+ decoded = JSON.parse(input);
81
+ }
82
+ catch {
83
+ return invalidCapabilitySet("invalid-json");
84
+ }
85
+ return parseEditorialCapabilities(decoded);
86
+ }
87
+ const READER_CAPABILITIES = [
88
+ "chapters:read",
89
+ "comments:read",
90
+ "suggestions:read",
91
+ ];
92
+ const CONTRIBUTOR_CAPABILITIES = [
93
+ ...READER_CAPABILITIES,
94
+ "comments:write",
95
+ "suggestions:write",
96
+ "replies:write",
97
+ "comments:vote",
98
+ "suggestions:vote",
99
+ "feedback:withdraw-own",
100
+ "summaries:write",
101
+ ];
102
+ const EDITOR_CAPABILITIES = [
103
+ ...CONTRIBUTOR_CAPABILITIES,
104
+ "work:read",
105
+ "work:claim",
106
+ "work:submit",
107
+ "chapters:write",
108
+ "revisions:read",
109
+ "revisions:write",
110
+ "history:read",
111
+ ];
112
+ const MAINTAINER_CAPABILITIES = EDITORIAL_CAPABILITIES;
113
+ /**
114
+ * Maximum canonical authority admitted by each current project role.
115
+ * Token grants and this ceiling are independent; effective authority is their
116
+ * intersection.
117
+ */
118
+ export const ROLE_EDITORIAL_CAPABILITIES = Object.freeze({
119
+ reader: READER_CAPABILITIES,
120
+ contributor: CONTRIBUTOR_CAPABILITIES,
121
+ editor: EDITOR_CAPABILITIES,
122
+ maintainer: MAINTAINER_CAPABILITIES,
123
+ });
124
+ export function roleEditorialCapabilities(role) {
125
+ return ROLE_EDITORIAL_CAPABILITIES[role];
126
+ }
127
+ /** Token grants intersected with the actor's current role, in canonical order. */
128
+ export function effectiveEditorialCapabilities(grantedCapabilities, role) {
129
+ const granted = new Set(grantedCapabilities);
130
+ const ceiling = new Set(ROLE_EDITORIAL_CAPABILITIES[role]);
131
+ return EDITORIAL_CAPABILITIES.filter((capability) => granted.has(capability) && ceiling.has(capability));
132
+ }
133
+ /** Enforce the independent exact-grant and current-role checks. */
134
+ export function authorizeEditorialCapability(grantedCapabilities, role, required) {
135
+ if (!grantedCapabilities.includes(required)) {
136
+ return denied("missing-capability", `token lacks required editorial capability "${required}"`);
137
+ }
138
+ if (!ROLE_EDITORIAL_CAPABILITIES[role].includes(required)) {
139
+ return denied("role-ceiling", `project role "${role}" does not admit editorial capability "${required}"`);
140
+ }
141
+ return ALLOWED;
142
+ }
143
+ /** Legacy names that exercised editorial authority before canonical mode. */
144
+ export const LEGACY_EDITORIAL_SCOPES = [
145
+ "chapters:read",
146
+ "annotations:read",
147
+ "annotations:write",
148
+ "work:read",
149
+ "work:claim",
150
+ "submissions:write",
151
+ "votes:write",
152
+ ];
153
+ /** Known old control-plane names. They never translate to editorial authority. */
154
+ export const LEGACY_CONTROL_SCOPES = ["tokens:manage", "members:manage"];
155
+ export const LEGACY_AGENT_SCOPES = [
156
+ "chapters:read",
157
+ "annotations:read",
158
+ "annotations:write",
159
+ "work:read",
160
+ "work:claim",
161
+ "submissions:write",
162
+ ...LEGACY_CONTROL_SCOPES,
163
+ "votes:write",
164
+ ];
165
+ export const legacyAgentScopeSchema = z.enum(LEGACY_AGENT_SCOPES);
166
+ const LEGACY_TRANSLATION = Object.freeze({
167
+ "chapters:read": ["chapters:read"],
168
+ "annotations:read": ["comments:read", "suggestions:read"],
169
+ "annotations:write": [
170
+ "comments:write",
171
+ "suggestions:write",
172
+ "replies:write",
173
+ "feedback:withdraw-own",
174
+ ],
175
+ "work:read": ["work:read"],
176
+ "work:claim": ["work:claim"],
177
+ "submissions:write": ["work:submit", "chapters:write", "chapters:publish"],
178
+ "votes:write": ["suggestions:vote"],
179
+ });
180
+ /**
181
+ * Translate safe, known legacy editorial names. Unknown and control-plane
182
+ * names never produce authority, and prerequisites are deliberately not
183
+ * synthesized.
184
+ */
185
+ export function translateLegacyScopes(scopes) {
186
+ const present = new Set(scopes);
187
+ const translated = new Set();
188
+ for (const scope of LEGACY_EDITORIAL_SCOPES) {
189
+ if (!present.has(scope))
190
+ continue;
191
+ for (const capability of LEGACY_TRANSLATION[scope]) {
192
+ translated.add(capability);
193
+ }
194
+ }
195
+ return EDITORIAL_CAPABILITIES.filter((capability) => translated.has(capability));
196
+ }
197
+ /**
198
+ * Sanitize a decoded legacy scope set for dual-read mode.
199
+ *
200
+ * Known control-plane and unknown string names are returned for audit and
201
+ * removed. Structurally malformed input denies the complete set so a corrupt
202
+ * row cannot partially authorize a request.
203
+ */
204
+ export function parseLegacyScopes(input) {
205
+ if (!Array.isArray(input) || !input.every((scope) => typeof scope === "string")) {
206
+ return {
207
+ ok: false,
208
+ scopes: [],
209
+ removedScopes: [],
210
+ capabilities: [],
211
+ reason: "invalid-legacy-scope-set",
212
+ };
213
+ }
214
+ const values = input;
215
+ const present = new Set(values);
216
+ const scopes = LEGACY_EDITORIAL_SCOPES.filter((scope) => present.has(scope));
217
+ const safe = new Set(LEGACY_EDITORIAL_SCOPES);
218
+ const removedScopes = [...new Set(values.filter((scope) => !safe.has(scope)))];
219
+ return {
220
+ ok: true,
221
+ scopes,
222
+ removedScopes,
223
+ capabilities: translateLegacyScopes(scopes),
224
+ };
225
+ }
226
+ export const LEGACY_COMPATIBILITY_ACTIONS = [
227
+ "feedback:moderate",
228
+ "work:promote",
229
+ "work:cancel",
230
+ ];
231
+ /**
232
+ * Old maintainer-only actions preserved only while a row remains in legacy
233
+ * mode. They are reported separately and are never canonical grants.
234
+ */
235
+ export function legacyEffectiveActions(scopes, role) {
236
+ if (role !== "maintainer")
237
+ return [];
238
+ const present = new Set(scopes);
239
+ const actions = [];
240
+ if (present.has("annotations:write")) {
241
+ actions.push({
242
+ action: "feedback:moderate",
243
+ source: "legacy-scope",
244
+ sourceScope: "annotations:write",
245
+ });
246
+ }
247
+ if (present.has("work:claim")) {
248
+ actions.push({
249
+ action: "work:promote",
250
+ source: "legacy-scope",
251
+ sourceScope: "work:claim",
252
+ }, {
253
+ action: "work:cancel",
254
+ source: "legacy-scope",
255
+ sourceScope: "work:claim",
256
+ });
257
+ }
258
+ return actions;
259
+ }
260
+ /**
261
+ * Requirements for writing an old scope alongside a canonical grant set.
262
+ *
263
+ * These are evaluated against granted capabilities, not today's role. A role
264
+ * may be raised later without rewriting the token, so the shadow must remain
265
+ * safe at the maintainer ceiling. Prerequisites used only by canonical routes
266
+ * are included where an old umbrella route did not enforce them.
267
+ */
268
+ const LEGACY_SHADOW_REQUIREMENTS = Object.freeze({
269
+ "chapters:read": ["chapters:read"],
270
+ "annotations:read": ["comments:read", "suggestions:read"],
271
+ "annotations:write": [
272
+ "chapters:read",
273
+ "comments:read",
274
+ "suggestions:read",
275
+ "comments:write",
276
+ "suggestions:write",
277
+ "replies:write",
278
+ "feedback:withdraw-own",
279
+ "feedback:moderate",
280
+ ],
281
+ "work:read": ["work:read"],
282
+ "work:claim": ["work:promote", "work:claim", "work:cancel"],
283
+ "submissions:write": ["work:submit", "chapters:write", "chapters:publish"],
284
+ "votes:write": ["suggestions:vote"],
285
+ });
286
+ /**
287
+ * Conservative old-scope shadow for a canonical row.
288
+ *
289
+ * A prior Worker may deny a granular action after deploy or rollback, but
290
+ * every scope it does see is guaranteed not to exceed the canonical grant.
291
+ * Control-plane and unknown names can never be emitted.
292
+ */
293
+ export function legacyScopeShadow(capabilities) {
294
+ const granted = new Set(capabilities);
295
+ return LEGACY_EDITORIAL_SCOPES.filter((scope) => LEGACY_SHADOW_REQUIREMENTS[scope].every((capability) => granted.has(capability)));
296
+ }
297
+ //# sourceMappingURL=editorial-capabilities.js.map
package/dist/index.d.ts CHANGED
@@ -4,12 +4,14 @@ export type { AnnotationPolicy, AnnotationPolicyDenialReason, AnnotationPolicyRe
4
4
  export type { Allowed, Decision, Denied } from "./decision.js";
5
5
  export { ROLES, ROLE_SCOPES, SCOPES, effectiveScopes, requireScope, requireScopes, roleSchema, roleScopes, scopeSchema, } from "./scopes.js";
6
6
  export type { Role, Scope, ScopeDenialReason } from "./scopes.js";
7
+ export { EDITORIAL_CAPABILITIES, LEGACY_AGENT_SCOPES, LEGACY_COMPATIBILITY_ACTIONS, LEGACY_CONTROL_SCOPES, LEGACY_EDITORIAL_SCOPES, ROLE_EDITORIAL_CAPABILITIES, authorizeEditorialCapability, editorialCapabilitiesSchema, editorialCapabilitySchema, effectiveEditorialCapabilities, legacyAgentScopeSchema, legacyEffectiveActions, legacyScopeShadow, parseEditorialCapabilities, parseLegacyScopes, parseStoredEditorialCapabilities, roleEditorialCapabilities, translateLegacyScopes, } from "./editorial-capabilities.js";
8
+ export type { EditorialCapability, EditorialCapabilityDenialReason, EditorialCapabilityParseFailureReason, EditorialCapabilityParseResult, LegacyAgentScope, LegacyCompatibilityAction, LegacyControlScope, LegacyEditorialScope, LegacyEffectiveAction, LegacyScopeParseResult, } from "./editorial-capabilities.js";
7
9
  export { ANNOTATION_STATUSES, ANNOTATION_TRANSITIONS, authorizeAnnotationWithdraw, canTransitionAnnotation, transitionAnnotation, } from "./annotation-state.js";
8
10
  export type { AnnotationStatus, AnnotationTransitionDenialReason, WithdrawDenialReason, } from "./annotation-state.js";
9
11
  export { GIT_OPERATION_STATES, GIT_OPERATION_TRANSITIONS, INITIAL_GIT_OPERATION, MAX_GIT_ATTEMPTS, canRetryGitOperation, canTransitionGitOperation, isGitOperationTerminal, transitionGitOperation, } from "./git-operation-state.js";
10
12
  export type { GitOperationDenialReason, GitOperationProgress, GitOperationState, GitOperationTransitionResult, } from "./git-operation-state.js";
11
- export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, } from "./commands.js";
12
- export type { CreateAnnotationCommand, CreateReplyCommand, MintAgentTokenCommand, MintAgentTokenCommandInput, WithdrawAnnotationCommand, } from "./commands.js";
13
+ export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, withdrawReplyCommandSchema, } from "./commands.js";
14
+ export type { CreateAnnotationCommand, CreateReplyCommand, MintAgentTokenCommand, MintAgentTokenCommandInput, WithdrawAnnotationCommand, WithdrawReplyCommand, } from "./commands.js";
13
15
  export { VOTE_VALUES, authorizeVote, castVoteCommandSchema, clearVoteCommandSchema, voteValueSchema, } from "./vote.js";
14
16
  export type { CastVoteCommand, ClearVoteCommand, VoteDenialReason, VoteValue } from "./vote.js";
15
17
  export { DECISION_RESULTS, DECISION_SUPPORT_CHANGED_EVENT, decisionResultSchema, resolveSupportChange, } from "./decision-support.js";
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export { ALLOWED, denied } from "./decision.js";
2
2
  export { ANNOTATION_POLICIES, DEFAULT_ANNOTATION_POLICY, checkAnnotationPolicy, isAnnotationPolicy, policyRequiresApproval, } from "./annotation-policy.js";
3
3
  export { ROLES, ROLE_SCOPES, SCOPES, effectiveScopes, requireScope, requireScopes, roleSchema, roleScopes, scopeSchema, } from "./scopes.js";
4
+ export { EDITORIAL_CAPABILITIES, LEGACY_AGENT_SCOPES, LEGACY_COMPATIBILITY_ACTIONS, LEGACY_CONTROL_SCOPES, LEGACY_EDITORIAL_SCOPES, ROLE_EDITORIAL_CAPABILITIES, authorizeEditorialCapability, editorialCapabilitiesSchema, editorialCapabilitySchema, effectiveEditorialCapabilities, legacyAgentScopeSchema, legacyEffectiveActions, legacyScopeShadow, parseEditorialCapabilities, parseLegacyScopes, parseStoredEditorialCapabilities, roleEditorialCapabilities, translateLegacyScopes, } from "./editorial-capabilities.js";
4
5
  export { ANNOTATION_STATUSES, ANNOTATION_TRANSITIONS, authorizeAnnotationWithdraw, canTransitionAnnotation, transitionAnnotation, } from "./annotation-state.js";
5
6
  export { GIT_OPERATION_STATES, GIT_OPERATION_TRANSITIONS, INITIAL_GIT_OPERATION, MAX_GIT_ATTEMPTS, canRetryGitOperation, canTransitionGitOperation, isGitOperationTerminal, transitionGitOperation, } from "./git-operation-state.js";
6
- export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, } from "./commands.js";
7
+ export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, withdrawReplyCommandSchema, } from "./commands.js";
7
8
  export { VOTE_VALUES, authorizeVote, castVoteCommandSchema, clearVoteCommandSchema, voteValueSchema, } from "./vote.js";
8
9
  export { DECISION_RESULTS, DECISION_SUPPORT_CHANGED_EVENT, decisionResultSchema, resolveSupportChange, } from "./decision-support.js";
9
10
  export { PHASE3_WORK_ITEM_STATUSES, WORK_ITEM_STATUSES, WORK_ITEM_TRANSITIONS, canTransitionWorkItem, isPhase3WorkItemStatus, transitionWorkItem, } from "./work-item-state.js";
package/dist/vote.d.ts CHANGED
@@ -5,7 +5,7 @@ import { type Decision } from "./decision.js";
5
5
  * Vote commands (Phase 3 contract section 2):
6
6
  * `PUT /v1/projects/{p}/annotations/{id}/vote` with `{ value }`, `DELETE`
7
7
  * clears. One current vote per actor is a DB uniqueness concern (upsert);
8
- * this module pins the command shapes and the suggestion-only rule.
8
+ * this module pins the command shapes and kind support.
9
9
  */
10
10
  /** Vote values (contract section 2; mirrors `authorbot.instance/v1` `votes.values`). */
11
11
  export declare const VOTE_VALUES: readonly ["approve", "reject", "abstain"];
@@ -30,13 +30,12 @@ export declare const clearVoteCommandSchema: z.ZodObject<{
30
30
  annotationId: z.ZodString;
31
31
  }, z.core.$strict>;
32
32
  export type ClearVoteCommand = z.infer<typeof clearVoteCommandSchema>;
33
- export type VoteDenialReason = "not-a-suggestion";
33
+ export type VoteDenialReason = never;
34
34
  /**
35
- * Suggestion-only guard (contract section 2: votes on comments → 422). Votes
36
- * stay legal after `open` - sticky decisions require tracking vote changes on
37
- * `work_item_created` annotations (contract section 4) - so annotation status
38
- * is deliberately not checked here; `votes:write` scope enforcement is the
39
- * API layer's (`requireScope`).
35
+ * Both comments and suggestions use the same one-current-vote resource. The
36
+ * API applies the exact kind capability and keeps comment tallies out of the
37
+ * suggestion-to-Work rule. Status is deliberately checked by that serialized
38
+ * API command because suggestion votes remain legal after a sticky crossing.
40
39
  */
41
40
  export declare function authorizeVote(input: {
42
41
  annotationKind: AnnotationKind;
package/dist/vote.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { uuidv7Schema } from "@authorbot/schemas";
3
- import { ALLOWED, denied } from "./decision.js";
3
+ import { ALLOWED } from "./decision.js";
4
4
  /**
5
5
  * Vote commands (Phase 3 contract section 2):
6
6
  * `PUT /v1/projects/{p}/annotations/{id}/vote` with `{ value }`, `DELETE`
7
7
  * clears. One current vote per actor is a DB uniqueness concern (upsert);
8
- * this module pins the command shapes and the suggestion-only rule.
8
+ * this module pins the command shapes and kind support.
9
9
  */
10
10
  /** Vote values (contract section 2; mirrors `authorbot.instance/v1` `votes.values`). */
11
11
  export const VOTE_VALUES = ["approve", "reject", "abstain"];
@@ -20,16 +20,13 @@ export const clearVoteCommandSchema = z.strictObject({
20
20
  annotationId: uuidv7Schema,
21
21
  });
22
22
  /**
23
- * Suggestion-only guard (contract section 2: votes on comments → 422). Votes
24
- * stay legal after `open` - sticky decisions require tracking vote changes on
25
- * `work_item_created` annotations (contract section 4) - so annotation status
26
- * is deliberately not checked here; `votes:write` scope enforcement is the
27
- * API layer's (`requireScope`).
23
+ * Both comments and suggestions use the same one-current-vote resource. The
24
+ * API applies the exact kind capability and keeps comment tallies out of the
25
+ * suggestion-to-Work rule. Status is deliberately checked by that serialized
26
+ * API command because suggestion votes remain legal after a sticky crossing.
28
27
  */
29
28
  export function authorizeVote(input) {
30
- if (input.annotationKind !== "suggestion") {
31
- return denied("not-a-suggestion", "votes apply to suggestions only; comments cannot be voted on");
32
- }
29
+ void input;
33
30
  return ALLOWED;
34
31
  }
35
32
  //# sourceMappingURL=vote.js.map
@@ -13,7 +13,7 @@ import { type Decision, type Denied } from "./decision.js";
13
13
  * `expire` and `release` are the two expiry/release back-edges sharing
14
14
  * `leased -> ready`, and `cancel` covers both maintainer-action arrows.
15
15
  */
16
- export declare const WORK_ITEM_TRIGGERS: readonly ["claim", "expire", "release", "submit", "validation_passed", "validation_failed", "apply_succeeded", "apply_conflicted", "conflict_resolution_prepared", "cancel"];
16
+ export declare const WORK_ITEM_TRIGGERS: readonly ["claim", "expire", "release", "submit", "review_rejected", "validation_passed", "validation_failed", "apply_succeeded", "apply_conflicted", "conflict_resolution_prepared", "cancel"];
17
17
  export type WorkItemTrigger = (typeof WORK_ITEM_TRIGGERS)[number];
18
18
  /** Trigger -> the design edges it may traverse (exactly section 9.5). */
19
19
  export declare const WORK_ITEM_TRIGGER_EDGES: Readonly<Record<WorkItemTrigger, ReadonlyArray<readonly [WorkItemStatus, WorkItemStatus]>>>;
@@ -19,6 +19,7 @@ export const WORK_ITEM_TRIGGERS = [
19
19
  "expire",
20
20
  "release",
21
21
  "submit",
22
+ "review_rejected",
22
23
  "validation_passed",
23
24
  "validation_failed",
24
25
  "apply_succeeded",
@@ -32,6 +33,7 @@ export const WORK_ITEM_TRIGGER_EDGES = Object.freeze({
32
33
  expire: [["leased", "ready"]],
33
34
  release: [["leased", "ready"]],
34
35
  submit: [["leased", "submitted"]],
36
+ review_rejected: [["submitted", "ready"]],
35
37
  validation_passed: [["submitted", "applying"]],
36
38
  validation_failed: [["submitted", "failed"]],
37
39
  apply_succeeded: [["applying", "completed"]],
@@ -12,7 +12,9 @@ export { WORK_ITEM_STATUSES };
12
12
  export const WORK_ITEM_TRANSITIONS = Object.freeze({
13
13
  ready: ["leased", "cancelled"],
14
14
  leased: ["ready", "submitted", "cancelled"],
15
- submitted: ["applying", "failed"],
15
+ // Phase 11 review-gated chapter revisions can be declined without burning
16
+ // the Work item. The lease is already consumed, so it returns claimable.
17
+ submitted: ["ready", "applying", "failed"],
16
18
  applying: ["completed", "conflict"],
17
19
  conflict: ["ready"],
18
20
  completed: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authorbot/domain",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Pure domain logic for Authorbot: role/scope authorization, annotation and Git-operation state machines, command validators, and token/session value rules (Phase 2 contract section 3)",
5
5
  "keywords": [
6
6
  "authorbot",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "zod": "^4.0.0",
43
- "@authorbot/schemas": "0.1.33"
43
+ "@authorbot/schemas": "0.1.35"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.0.0",