@badger_badger/badger_hq 0.0.0-stage → 1.6.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 ADDED
@@ -0,0 +1,128 @@
1
+ # badger_hq
2
+
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d6d284c: Two task kinds with their own shape.
8
+
9
+ - `Task.target?` (`TaskTargetSchema`): the one source a task points at — `{ sourceId?, platform, url, label?, accessIssue?, note? }`. Required on Monitor tasks by hero-service; optional on Discovery tasks (search inside this source).
10
+ - `Task.captureWindow?` (`CaptureWindowSchema`): what a Monitor task captures — `{ mode: 'last_days', days: 1..90 }` or `{ mode: 'range', start, end }` with `end > start`. Stored flat in Mongo (`captureWindowSchema`), validated as a discriminated union in Zod.
11
+ - New enums `ACCESS_ISSUE_VALUES` (`login_required`, `private`, `bot_detection`, `captcha`, `geo_blocked`, `rate_limited`, `not_found`, `no_collector`, `collector_full`, `other`) and `CAPTURE_WINDOW_MODE_VALUES`.
12
+ - `AgentTaskSchema` gains `target?` (as `AgentTaskTargetSchema`, without `sourceId`) and `captureWindow?`.
13
+ - `AgentSession.captureWindow?` (`{ start, end }`): the task's window resolved to dates when the session started.
14
+ - `AgentSessionViewSchema` gains `target?` and `captureWindow?` (resolved) so the session header can show what is being captured.
15
+ - Task-tagged captures (agent-service analyses them in the background, with no live session):
16
+ - `Capture.sessionId` is now optional.
17
+ - New optional `assignmentId`, `taskId`, `missionId` and `userId`.
18
+ - New `attempts` (default 0) and `claimedAt` for the capture worker.
19
+ - New indexes `{status, createdAt}` and `{orgId, assignmentId, userId, capturedAt}`.
20
+ - `HoursLog.assignmentId?` and `HoursLog.source?` (`HOURS_LOG_SOURCE_VALUES`: `manual`, `inferred`), plus a unique partial index for one inferred row per person, assignment and day.
21
+
22
+ All fields are optional; existing documents and consumers are unaffected.
23
+
24
+ Validation:
25
+
26
+ - `TaskTargetSchema.url` accepts only `http:` / `https:` URLs (new `isHttpUrl` helper).
27
+ - `CaptureWindowSchema` `range` dates must be a `Date` or an ISO 8601 string with an explicit offset; `end` is exclusive and the range is at most `MAX_CAPTURE_WINDOW_DAYS` long.
28
+ - New index `{ orgId, target.sourceId }` on `tasks` (partial on `target.sourceId`).
29
+
30
+ ## 1.4.0
31
+
32
+ ### Minor Changes
33
+
34
+ - Badger Agent runtime contracts.
35
+
36
+ - New entities and collections: `AgentSession` (`agentSessions`), `AgentMessage` (`agentMessages`, with the `AgentCard` discriminated union), `Capture` (`captures`, screenshot + read-only DOM snapshot refs, focus region, retention `expiresAt`) and `LlmCall` (`llmCalls`).
37
+ - New wire contracts in `zod/agent.ts`: session views, `AgentSessionStartBody`, `AgentReplyBody`, `AgentCaptureFields`, `AgentCaptureResponse`, `AgentStreamEvent` (SSE), `CaptureDomSnapshot`; new `AgentErrorCode` values (`FORBIDDEN`, `CONFLICT`, `SESSION_NOT_ACTIVE`, `PAYLOAD_TOO_LARGE`, `BUDGET_EXCEEDED`, `LLM_UNAVAILABLE`, `UPSTREAM_UNAVAILABLE`).
38
+ - New `zod/internal.ts`: service-to-service contracts for hero-service `/internal/sources/intake`, `/internal/sources/:id/visit`, `/internal/data-items/priority`.
39
+ - New enums: `AGENT_SESSION_MODE`, `AGENT_SESSION_STATUS`, `AGENT_MESSAGE_ROLE`, `AGENT_MESSAGE_KIND`, `AGENT_CARD_KIND`, `AGENT_SOURCE_OUTCOME`, `CAPTURE_STATUS`, `CAPTURE_DOM_STATUS`, `LLM_CAPABILITY`, `LLM_PROVIDER`, `LLM_OUTCOME`, `SOURCE_INTAKE_OUTCOME`.
40
+ - `SOURCE_STATUS_VALUES` gains `access_problem` (monitor visit hit a login wall; parked for review).
41
+ - `DataItem.contentHash?` with a partial unique index `(orgId, sourceId, contentHash)`.
42
+
43
+ ## 1.2.0
44
+
45
+ ### Minor Changes
46
+
47
+ - 3740d46: Add Badger Agent wire contracts (`src/zod/agent.ts`): `AgentRole`, `AgentMeSchema`, `AgentTaskSchema`,
48
+ `AgentAssignmentItemSchema`, `AgentAssignmentsResponseSchema`, `AgentAssignmentVersionsResponseSchema`,
49
+ `AgentErrorSchema`. These are read models shared by `agent-service` and the Chrome extension.
50
+ - 404cbc3: Initial release (0.1.0) of the shared Harvester V2 schema package.
51
+
52
+ - Browser-safe entry `.`: Zod schemas, inferred types, enums, `COLLECTIONS`, pagination and `ApiError` helpers.
53
+ - Server entry `./mongo`: Mongoose schemas typed against the Zod types, index definitions, `registerModels(connection)`,
54
+ `incrementTaskVersion` hook and the `objectIdsToStrings` `toJSON` transform.
55
+ - Entities: Mission, Task, Assignment, HoursLog, ScoutProfile, Source, SourceImport, DataItem, Scraper, ScraperMetric,
56
+ Deployment, AuditLog. Identity (users, orgs, memberships, roles) is owned by Clerk and referenced by id strings only.
57
+
58
+ - Add `fullName` to `ScoutProfile`: the scout's real name, System- and self-only (organizations keep seeing
59
+ `displayName`, which may be an alias). Optional on stored profiles, required in `ScoutProfileCreateSchema`,
60
+ editable through `ScoutSelfUpdateSchema`.
61
+
62
+ ## 1.0.0
63
+
64
+ ### Major Changes
65
+
66
+ - Scouts and analysts as assignable people; multiple assignees per task.
67
+
68
+ **Changed**
69
+
70
+ - `ScoutProfile`: `availability { tz, windows }` became `timezone` (IANA string) + `availability` (array of weekly
71
+ windows); `platforms` became `platformAccess { all, platforms, special[] }`; the single `rating` became
72
+ `ratingSummary { avg, count }` (denormalized from `scoutRatings`). New fields: `email` (unique, lowercased),
73
+ `status` (`active | disabled`), `systemRecommendation { taskKinds, tags, note, updatedBy, updatedAt }`.
74
+ `ScoutProfileCreateSchema` now includes `email` and `vettingStatus`; `ScoutSelfUpdateSchema` lists what a scout
75
+ may edit about themselves.
76
+ - `Assignment`: the partial unique index on `{ taskId }` (status `active`) is **removed**; a task may now hold many
77
+ active assignees. New partial unique index `{ taskId, assigneeUserId }` (status `active`), new index
78
+ `{ assigneeUserId, status, orgId }` (replaces `{ assigneeUserId, status }`), new optional `revokedBy`.
79
+
80
+ **Added**
81
+
82
+ - `AnalystProfile` (`analystProfiles`, unique `{ orgId, userId }`), `OrgSettings` (`orgSettings`, unique `{ orgId }`,
83
+ `DEFAULT_ANALYST_TASK_LIMIT = 5`), `ScoutRating` (`scoutRatings`), `AssigneeLoad` (`assigneeLoads`, unique
84
+ `{ userId, scope }`).
85
+ - Shared matching fragments: `MatchingFieldsSchema`, `AvailabilityWindowSchema`, `PlatformAccessSchema`,
86
+ `SpecialAccessSchema`, `TimeZone`; `RatingScore`, `RatingSummarySchema`, `SystemRecommendationSchema`, `Email`,
87
+ `LoadScope` / `GLOBAL_LOAD_SCOPE`, `MAX_RATING_COMMENT_LENGTH`.
88
+ - Enums `ScoutStatus` (`SCOUT_STATUS_VALUES`) and `RaterType` (`RATER_TYPE_VALUES`).
89
+ - `ObjectIdKey` includes `assignmentId`.
90
+
91
+ **Migration**
92
+
93
+ 1. `scoutProfiles`: for each document set `timezone = availability.tz`, `availability = availability.windows`,
94
+ `platformAccess = { all: false, platforms, special: [] }`, `ratingSummary = { avg: rating ?? null, count: rating ? 1 : 0 }`,
95
+ `status = 'active'`, `email` = the Clerk user's primary email (lowercased); unset `platforms` and `rating`.
96
+ 2. `assignments`: drop the index on `{ taskId: 1 }` with `partialFilterExpression { status: 'active', deletedAt: null }`
97
+ and the index `{ assigneeUserId: 1, status: 1 }`, then `syncIndexes()`.
98
+ 3. Seed `assigneeLoads` from active assignments: for scouts `{ userId, scope: 'global', active }`, for analysts
99
+ `{ userId, scope: orgId, active }`.
100
+ 4. `orgSettings` and `analystProfiles` are created lazily by hero-service (Clerk webhooks); no backfill needed.
101
+
102
+ ### Minor Changes
103
+
104
+ - d6e348c: Add the `Database` entity: one per organization, holding the `name` and `connectionString` of the org's Mongo
105
+ database (where `sources`, `dataItems`, … live). Ships `DatabaseSchema` / `DatabaseCreateSchema` /
106
+ `DatabaseUpdateSchema`, the `DatabaseStatus` and `DatabaseCollection` enums, `COLLECTIONS.databases`, the
107
+ `databaseSchema` Mongoose schema with a unique partial index on `orgId`, and the `Database` model in
108
+ `registerModels()`.
109
+ - Add a server-managed `databaseId` to `Source` and `DataItem` (new `DatabaseId` scalar in `common`). All
110
+ organizations' collected data lives in the shared service database, partitioned by `(orgId, databaseId)`;
111
+ today `databaseId` equals the Clerk org id, and it becomes the id of a physical `Database` record once orgs are
112
+ split into their own databases. `databaseId` is omitted from the `*CreateSchema`s and listed in
113
+ `SERVER_MANAGED_KEYS`; the `sources` and `dataItems` collections gain an `{ orgId, databaseId }` index. No
114
+ `Database` records are created any more — the entity is kept for the future split.
115
+ - 404cbc3: Initial release (0.1.0) of the shared Harvester V2 schema package.
116
+
117
+ - Browser-safe entry `.`: Zod schemas, inferred types, enums, `COLLECTIONS`, pagination and `ApiError` helpers.
118
+ - Server entry `./mongo`: Mongoose schemas typed against the Zod types, index definitions, `registerModels(connection)`,
119
+ `incrementTaskVersion` hook and the `objectIdsToStrings` `toJSON` transform.
120
+ - Entities: Mission, Task, Assignment, HoursLog, ScoutProfile, Source, SourceImport, DataItem, Scraper, ScraperMetric,
121
+ Deployment, AuditLog. Identity (users, orgs, memberships, roles) is owned by Clerk and referenced by id strings only.
122
+
123
+ ### Patch Changes
124
+
125
+ - d6e348c: Extend the `Platform` enum (`PLATFORM_VALUES`) with `instagram`, `youtube`, `whatsapp`, `reddit`, `discord`,
126
+ `linkedin`, `threads`, `vk`, `snapchat` and `web` (open web: sites, forums, blogs). Existing values are unchanged.
127
+
128
+ Release notes are generated from `.changeset/` by `changeset version`.
package/README.md CHANGED
@@ -1,3 +1,280 @@
1
- # Temporary Holding Version
1
+ # @badger_badger/badger_hq
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared types and schemas for **Harvester V2** (Badger HQ). Every service imports it: the Next.js web app,
4
+ the Chrome extension, the Fastify core API, the worker, the agent and the scrapers.
5
+
6
+ It ships two entry points:
7
+
8
+ | Entry | Import | Contents | Safe in |
9
+ |---|---|---|---|
10
+ | `.` | `@badger_badger/badger_hq` | Zod schemas, inferred TS types, enums, `COLLECTIONS`, pagination / `ApiError` helpers | Browser **and** Node |
11
+ | `./mongo` | `@badger_badger/badger_hq/mongo` | Mongoose schemas, index definitions, `registerModels(connection)` | Node only |
12
+
13
+ Nothing reachable from `@badger_badger/badger_hq` imports `mongoose`, `bson` or a Node built-in; a test bundles it with esbuild
14
+ for `platform: 'browser'` and fails if that ever changes.
15
+
16
+ ## Install
17
+
18
+ The package is published publicly to the npm registry.
19
+
20
+ ```sh
21
+ npm install @badger_badger/badger_hq zod
22
+ # server-side packages also need:
23
+ npm install mongoose
24
+ ```
25
+
26
+ `zod` (v3) is a required peer dependency. `mongoose` (v8) is an **optional** peer: browser consumers don't install it.
27
+
28
+ ## Entry point `.` — Zod + types
29
+
30
+ ```ts
31
+ import {
32
+ MissionCreateSchema,
33
+ type MissionCreate,
34
+ type Mission,
35
+ Platform,
36
+ PLATFORM_VALUES,
37
+ PaginationQuerySchema,
38
+ type Paginated,
39
+ type ApiError,
40
+ } from '@badger_badger/badger_hq';
41
+ ```
42
+
43
+ Per entity (`Mission` shown; the same pattern applies to every entity):
44
+
45
+ | Export | Meaning |
46
+ |---|---|
47
+ | `MissionSchema` | Full document as it comes back from the API (`_id`, `orgId`, timestamps, …). |
48
+ | `MissionCreateSchema` | Body for `POST`. Omits `_id`, `orgId`, `createdAt`, `updatedAt`, `deletedAt` and server-set fields (e.g. `createdBy`). |
49
+ | `MissionUpdateSchema` | Body for `PATCH`. `MissionCreateSchema.partial()`. |
50
+ | `Mission`, `MissionCreate`, `MissionUpdate` | `z.infer` of the above. |
51
+ | `MissionCreateInput` | `z.input` — the pre-default shape, useful for form state (only where Create has defaults). |
52
+
53
+ Dates use `z.coerce.date()`, so JSON ISO strings parse to `Date`.
54
+
55
+ ### Example: React Hook Form resolver
56
+
57
+ ```tsx
58
+ import { useForm } from 'react-hook-form';
59
+ import { zodResolver } from '@hookform/resolvers/zod';
60
+ import { MissionCreateSchema, type MissionCreateInput, MISSION_PRIORITY_VALUES } from '@badger_badger/badger_hq';
61
+
62
+ export function NewMissionForm() {
63
+ const form = useForm<MissionCreateInput>({
64
+ resolver: zodResolver(MissionCreateSchema),
65
+ defaultValues: {
66
+ title: '',
67
+ description: '',
68
+ priority: 'medium',
69
+ keywords: [],
70
+ defaultCriteria: { languages: [], platforms: [], regions: [], regionMode: 'preferred', emergency: false },
71
+ },
72
+ });
73
+
74
+ return (
75
+ <form onSubmit={form.handleSubmit((values) => fetch('/api/missions', { method: 'POST', body: JSON.stringify(values) }))}>
76
+ <input {...form.register('title')} />
77
+ <select {...form.register('priority')}>
78
+ {MISSION_PRIORITY_VALUES.map((p) => <option key={p}>{p}</option>)}
79
+ </select>
80
+ {/* … */}
81
+ </form>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### Example: parsing an API response
87
+
88
+ ```ts
89
+ import { PaginatedSchema, TaskSchema } from '@badger_badger/badger_hq';
90
+
91
+ const PageOfTasks = PaginatedSchema(TaskSchema);
92
+ const page = PageOfTasks.parse(await res.json()); // page.items[0].createdAt is a Date
93
+ ```
94
+
95
+ ## Entry point `./mongo` — Mongoose
96
+
97
+ ```ts
98
+ import { registerModels, COLLECTIONS, INDEXES, incrementTaskVersion, type HarvesterModels } from '@badger_badger/badger_hq/mongo';
99
+ ```
100
+
101
+ The package **never connects**. You own the `mongoose.Connection`; `registerModels(conn)` registers the models on it
102
+ (idempotently — it reuses `conn.models[name]` if present) and returns them fully typed:
103
+
104
+ ```ts
105
+ { Database, Mission, Task, Assignment, HoursLog, ScoutProfile, AnalystProfile, OrgSettings, ScoutRating, AssigneeLoad,
106
+ Source, SourceImport, DataItem, Scraper, ScraperMetric, Deployment, AuditLog }
107
+ ```
108
+
109
+ People who can be assigned to tasks:
110
+
111
+ - **`ScoutProfile`** (global, not org-scoped) — a freelancer shared by every organization. Identity is the Clerk user
112
+ (`userId`, `email` mirror); the profile holds the matching data (`languages`, `regions`, `timezone`, weekly
113
+ `availability` windows, `platformAccess { all, platforms, special[] }`, `taskLimit`), System-only fields
114
+ (`hourlyRateUsd`, `systemRecommendation`) and the denormalized `ratingSummary { avg, count }`.
115
+ - **`AnalystProfile`** (org-scoped, one per `(orgId, userId)`) — the matching data of an org member with role
116
+ `org:analyst`. Same `MatchingFieldsSchema` as scouts, no rating or recommendation.
117
+ - **`OrgSettings`** (one per org) — `defaultAnalystTaskLimit` (5) and `timezone`.
118
+ - **`ScoutRating`** — one rating per `(assignmentId, raterUserId)`; System users may leave general ratings without
119
+ an assignment. `ScoutProfile.ratingSummary` is recomputed from this collection.
120
+ - **`AssigneeLoad`** — an atomic counter of active assignments per `(userId, scope)` where `scope` is `'global'` for
121
+ scouts and the `orgId` for analysts; task limits are enforced with a conditional `$inc` (`active < limit`).
122
+ - **`Assignment`** — a task may hold many active assignees; a person holds at most one active assignment per task
123
+ (unique partial index on `{ taskId, assigneeUserId }`).
124
+
125
+ `Database` is the per-organization data store: exactly one live document per `orgId` (unique partial index), holding
126
+ the `name` and `connectionString` of the Mongo database where the org's collected data (`sources`, `dataItems`, …)
127
+ lives. It is created automatically when an organization is created.
128
+
129
+ Each Mongoose schema is typed against its Zod type through `ToDoc<T>`, which swaps ObjectId-string fields for
130
+ `Types.ObjectId` and leaves Clerk ids as strings. Every schema sets `timestamps: true`, `versionKey: false`, an explicit
131
+ `collection` from `COLLECTIONS`, and a `toJSON`/`toObject` transform that converts ObjectIds to hex strings — so
132
+ `doc.toJSON()` parses with the matching Zod schema.
133
+
134
+ The Mongoose schema instances are exported in lowerCamelCase (`missionSchema`, `taskSchema`, …) so they never collide
135
+ with the Zod `MissionSchema`, `TaskSchema`, … from the browser entry.
136
+
137
+ ### Example: Fastify service
138
+
139
+ ```ts
140
+ import Fastify from 'fastify';
141
+ import mongoose from 'mongoose';
142
+ import { registerModels } from '@badger_badger/badger_hq/mongo';
143
+ import { MissionCreateSchema, MissionSchema, type Mission } from '@badger_badger/badger_hq';
144
+
145
+ const app = Fastify();
146
+ const conn = mongoose.createConnection(process.env.MONGO_URL!);
147
+ await conn.asPromise();
148
+ const models = registerModels(conn);
149
+ await Promise.all(Object.values(models).map((m) => m.syncIndexes())); // or run once in a migration step
150
+
151
+ app.post<{ Body: unknown; Reply: Mission }>('/missions', async (req, reply) => {
152
+ const body = MissionCreateSchema.parse(req.body);
153
+ const orgId = req.auth.orgId; // from @clerk/fastify
154
+ const createdBy = req.auth.userId; // from @clerk/fastify
155
+ const doc = await models.Mission.create({ ...body, orgId, createdBy });
156
+ return reply.code(201).send(MissionSchema.parse(doc.toJSON()));
157
+ });
158
+ ```
159
+
160
+ ### Indexes
161
+
162
+ `INDEXES` (in `./mongo`) lists every index per collection and is applied to the schemas at import time. Create them
163
+ with `Model.syncIndexes()` / `Model.createIndexes()` (your call — the package never talks to the database).
164
+
165
+ `orgId` is the first field of every compound index on org-scoped collections. Unique indexes on soft-deletable
166
+ collections use `partialFilterExpression: { deletedAt: null }` so soft-deleted rows never block re-creation (see
167
+ *Decisions*).
168
+
169
+ ### `Task.version`
170
+
171
+ `version` is bumped by `$inc` on every `findOneAndUpdate` / `updateOne` through the exported `incrementTaskVersion`
172
+ pre-hook (skipped when the update already sets/incs `version`, or is a pipeline update). Writes that bypass Mongoose
173
+ queries (raw driver, `updateMany`, `bulkWrite`) must `$inc: { version: 1 }` themselves.
174
+
175
+ ## Conventions
176
+
177
+ ### Clerk ids (identity lives in Clerk)
178
+
179
+ Clerk is the source of truth for users, organizations, memberships, roles, permissions and sessions. This package
180
+ contains **no** `User`, `Organization`, `Membership`, role or permission definitions and does not depend on `@clerk/*`.
181
+ Documents refer to Clerk entities by **id strings only**:
182
+
183
+ - `orgId` — Clerk org id, validated as `org_…` (`ClerkOrgId`).
184
+ - Every user reference (`createdBy`, `assigneeUserId`, `scoutUserId`, `actorUserId`, `approvedBy`, `doneBy`,
185
+ `assignedBy`, `requestedBy`, `userId`, …) — Clerk user id, validated as `user_…` (`ClerkUserId`).
186
+
187
+ They are plain `String` paths in Mongoose — never `ObjectId`, never `ref`.
188
+
189
+ ### Ids, timestamps, soft delete, org scoping
190
+
191
+ - Badger documents use Mongo ObjectIds: `ObjectIdString` (24-char hex) in Zod/API, `Schema.Types.ObjectId` in Mongoose.
192
+ - Every collection has `createdAt` / `updatedAt`.
193
+ - Every collection except `dataItems`, `scraperMetrics`, `orgSettings`, `scoutRatings` and `assigneeLoads` has an
194
+ optional `deletedAt` (soft delete).
195
+ - Every collection except the global ones — `scoutProfiles`, `scoutRatings` (optional `orgId`) and `assigneeLoads` —
196
+ has a required `orgId`.
197
+
198
+ ### Enums
199
+
200
+ Each enum is defined once in `enums.ts` as `z.enum([...])` plus a `*_VALUES` const array (e.g. `Platform` /
201
+ `PLATFORM_VALUES`). Mongoose schemas use the arrays for `enum:` validation, so Zod and Mongoose cannot drift.
202
+
203
+ ### Terminology
204
+
205
+ - **Mission** — a priority (PIR) in plain words; the top-level unit of work.
206
+ - **Task** — a unit of work under a mission, `source_discovery` or `monitor`; what gets assigned to a scout or analyst.
207
+ There is no "subtask" concept.
208
+ - **Discovery** — a free search: phrases (`keywords`), a short explanation (`description`), search queries, and
209
+ optionally one `target` source to search inside.
210
+ - **Monitor** — collect **one** `target` source a scraper can't (see `accessIssue`) over a `captureWindow`
211
+ (the last N days, or a fixed range).
212
+
213
+ ## Versioning
214
+
215
+ Semver, managed with [Changesets](https://github.com/changesets/changesets):
216
+
217
+ | Change | Bump |
218
+ |---|---|
219
+ | Add an optional field, add an enum value, add an index | `minor` |
220
+ | Remove or rename a field, change or remove an enum value, make an optional field required | `major` + a **Migration** note in the changeset |
221
+ | Docs, internal refactors, tightened validation with no shape change | `patch` |
222
+
223
+ Snapshot tests of the Zod shapes, Mongoose paths, enum values and index definitions fail the build on any unannounced
224
+ shape change; update the snapshot in the same PR as the changeset.
225
+
226
+ ### Release flow
227
+
228
+ 1. Open a PR with your change and `npm run changeset`.
229
+ 2. CI (`.github/workflows/ci.yml`) runs lint, typecheck, tests and build.
230
+ 3. On merge to `main`, `.github/workflows/release.yml` opens a *Version Packages* PR; merging it publishes to
231
+ GitHub Packages.
232
+
233
+ ## Development
234
+
235
+ ```sh
236
+ npm install
237
+ npm test # vitest (incl. mongodb-memory-server integration tests)
238
+ npm run lint
239
+ npm run typecheck
240
+ npm run build # tsup -> dist/ (ESM + CJS + .d.ts for both entry points)
241
+ npm pack --dry-run
242
+ ```
243
+
244
+ ## Decisions
245
+
246
+ Choices made where the spec left room, kept consistent with the rest of it:
247
+
248
+ 1. **Partial filter for soft-delete uniqueness.** MongoDB rejects `{ $exists: false }` in `partialFilterExpression`
249
+ (only `$exists: true`, equality, ranges, `$type`, `$and`/`$or`/`$in` are allowed). The equivalent supported form is
250
+ `{ deletedAt: null }`, which matches documents where `deletedAt` is missing or `null`. Used on `sources`,
251
+ `scoutProfiles`, `analystProfiles` and (combined with `status: 'active'`) `assignments`. Verified by integration test.
252
+ 2. **Server-set fields omitted from Create inputs**, per entity: Mission `createdBy`; Task `approvedBy/At`, `doneBy/At`,
253
+ `version`; Assignment `assignedBy`, `activatedAt`, `endedAt`, `revokedBy`; HoursLog `scoutUserId`; ScoutProfile
254
+ `userId`, `status`, `ratingSummary`, `systemRecommendation.updatedBy/At`; Source `discoveredBy`, `lastCollectedAt`;
255
+ SourceImport `status`, `summary`, `rowErrors`,
256
+ `createdBy`, `committedAt`; Scraper `sourceCount`, `status`, `deploymentId`; Deployment `status`, `steps`,
257
+ `requestedBy`; AuditLog `actorUserId`, `at`. Status transitions (approve, mark done, vet, revoke) are expected to be
258
+ dedicated endpoints rather than generic `PATCH`es.
259
+ 3. **Defaults** live on the base schemas so they apply to both parsing and inputs: statuses default to their initial
260
+ value (`draft`, `active`, `pending`, `validating`, `deploying`, `queued`), array fields default to `[]`,
261
+ `Task.version` to `1`, `TaskCriteria.emergency` and `AuditLog.actingAsSystem` to `false`.
262
+ 4. **`ScoutProfile.taskLimit`** is optional (absent ⇒ global default). The default is exported as
263
+ `DEFAULT_SCOUT_TASK_LIMIT = 3` instead of storing `null`; analysts fall back to
264
+ `OrgSettings.defaultAnalystTaskLimit` (`DEFAULT_ANALYST_TASK_LIMIT = 5`).
265
+ 5. **`Assignment.scoreBreakdown` / `AuditLog.diff`** are `Schema.Types.Mixed` in Mongoose (free-form), validated by Zod
266
+ as `Record<string, number>` / `unknown`.
267
+ 6. **Cross-org indexes.** `assignments { assigneeUserId, status, orgId }`, `auditLogs { actorUserId, at }`, the unique
268
+ `scraperMetrics { scraperId, hour }` and everything on the global `scoutProfiles`, `scoutRatings` and
269
+ `assigneeLoads` collections intentionally don't start with `orgId`: they are per-user / per-scraper lookups.
270
+ All other compound indexes start with `orgId`.
271
+ 7. **`ScraperMetric.errors`** is a reserved Mongoose pathname; the spec names the field so it is kept, with
272
+ `suppressReservedKeysWarning` set on that schema.
273
+ 8. **Mongoose schema export names** are lowerCamelCase (`missionSchema`) to avoid colliding with the Zod
274
+ `MissionSchema` when both entry points are imported in one file.
275
+ 9. **Package name** is `@badger_badger/badger_hq` (set in `package.json`), published publicly to the npm registry.
276
+ 10. **Multiple assignees per task (1.0.0).** The "one active assignee per task" unique index was replaced by
277
+ `{ taskId, assigneeUserId }` (partial, `status: 'active'`). Limits are enforced by `assigneeLoads`, not by an index,
278
+ because a scout's limit spans organizations while an analyst's is per org.
279
+ 11. **`ScoutRating.score`** is a Zod literal union `1 | 2 | 3 | 4 | 5` (and a Mongoose `Number` enum) rather than
280
+ `int().min(1).max(5)`, so the type itself documents the scale.