@badger_badger/badger_hq 0.0.0-stage → 1.5.1
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 +101 -0
- package/README.md +275 -2
- package/dist/agent-session-Bsn8W7Xv.d.cts +5384 -0
- package/dist/agent-session-Bsn8W7Xv.d.ts +5384 -0
- package/dist/index.cjs +1336 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2991 -0
- package/dist/index.d.ts +2991 -0
- package/dist/index.js +1104 -0
- package/dist/index.js.map +1 -0
- package/dist/mongo/index.cjs +1355 -0
- package/dist/mongo/index.cjs.map +1 -0
- package/dist/mongo/index.d.cts +8191 -0
- package/dist/mongo/index.d.ts +8191 -0
- package/dist/mongo/index.js +1299 -0
- package/dist/mongo/index.js.map +1 -0
- package/package.json +73 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# badger_hq
|
|
2
|
+
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Badger Agent runtime contracts.
|
|
8
|
+
|
|
9
|
+
- 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`).
|
|
10
|
+
- 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`).
|
|
11
|
+
- New `zod/internal.ts`: service-to-service contracts for hero-service `/internal/sources/intake`, `/internal/sources/:id/visit`, `/internal/data-items/priority`.
|
|
12
|
+
- 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`.
|
|
13
|
+
- `SOURCE_STATUS_VALUES` gains `access_problem` (monitor visit hit a login wall; parked for review).
|
|
14
|
+
- `DataItem.contentHash?` with a partial unique index `(orgId, sourceId, contentHash)`.
|
|
15
|
+
|
|
16
|
+
## 1.2.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 3740d46: Add Badger Agent wire contracts (`src/zod/agent.ts`): `AgentRole`, `AgentMeSchema`, `AgentTaskSchema`,
|
|
21
|
+
`AgentAssignmentItemSchema`, `AgentAssignmentsResponseSchema`, `AgentAssignmentVersionsResponseSchema`,
|
|
22
|
+
`AgentErrorSchema`. These are read models shared by `agent-service` and the Chrome extension.
|
|
23
|
+
- 404cbc3: Initial release (0.1.0) of the shared Harvester V2 schema package.
|
|
24
|
+
|
|
25
|
+
- Browser-safe entry `.`: Zod schemas, inferred types, enums, `COLLECTIONS`, pagination and `ApiError` helpers.
|
|
26
|
+
- Server entry `./mongo`: Mongoose schemas typed against the Zod types, index definitions, `registerModels(connection)`,
|
|
27
|
+
`incrementTaskVersion` hook and the `objectIdsToStrings` `toJSON` transform.
|
|
28
|
+
- Entities: Mission, Task, Assignment, HoursLog, ScoutProfile, Source, SourceImport, DataItem, Scraper, ScraperMetric,
|
|
29
|
+
Deployment, AuditLog. Identity (users, orgs, memberships, roles) is owned by Clerk and referenced by id strings only.
|
|
30
|
+
|
|
31
|
+
- Add `fullName` to `ScoutProfile`: the scout's real name, System- and self-only (organizations keep seeing
|
|
32
|
+
`displayName`, which may be an alias). Optional on stored profiles, required in `ScoutProfileCreateSchema`,
|
|
33
|
+
editable through `ScoutSelfUpdateSchema`.
|
|
34
|
+
|
|
35
|
+
## 1.0.0
|
|
36
|
+
|
|
37
|
+
### Major Changes
|
|
38
|
+
|
|
39
|
+
- Scouts and analysts as assignable people; multiple assignees per task.
|
|
40
|
+
|
|
41
|
+
**Changed**
|
|
42
|
+
|
|
43
|
+
- `ScoutProfile`: `availability { tz, windows }` became `timezone` (IANA string) + `availability` (array of weekly
|
|
44
|
+
windows); `platforms` became `platformAccess { all, platforms, special[] }`; the single `rating` became
|
|
45
|
+
`ratingSummary { avg, count }` (denormalized from `scoutRatings`). New fields: `email` (unique, lowercased),
|
|
46
|
+
`status` (`active | disabled`), `systemRecommendation { taskKinds, tags, note, updatedBy, updatedAt }`.
|
|
47
|
+
`ScoutProfileCreateSchema` now includes `email` and `vettingStatus`; `ScoutSelfUpdateSchema` lists what a scout
|
|
48
|
+
may edit about themselves.
|
|
49
|
+
- `Assignment`: the partial unique index on `{ taskId }` (status `active`) is **removed**; a task may now hold many
|
|
50
|
+
active assignees. New partial unique index `{ taskId, assigneeUserId }` (status `active`), new index
|
|
51
|
+
`{ assigneeUserId, status, orgId }` (replaces `{ assigneeUserId, status }`), new optional `revokedBy`.
|
|
52
|
+
|
|
53
|
+
**Added**
|
|
54
|
+
|
|
55
|
+
- `AnalystProfile` (`analystProfiles`, unique `{ orgId, userId }`), `OrgSettings` (`orgSettings`, unique `{ orgId }`,
|
|
56
|
+
`DEFAULT_ANALYST_TASK_LIMIT = 5`), `ScoutRating` (`scoutRatings`), `AssigneeLoad` (`assigneeLoads`, unique
|
|
57
|
+
`{ userId, scope }`).
|
|
58
|
+
- Shared matching fragments: `MatchingFieldsSchema`, `AvailabilityWindowSchema`, `PlatformAccessSchema`,
|
|
59
|
+
`SpecialAccessSchema`, `TimeZone`; `RatingScore`, `RatingSummarySchema`, `SystemRecommendationSchema`, `Email`,
|
|
60
|
+
`LoadScope` / `GLOBAL_LOAD_SCOPE`, `MAX_RATING_COMMENT_LENGTH`.
|
|
61
|
+
- Enums `ScoutStatus` (`SCOUT_STATUS_VALUES`) and `RaterType` (`RATER_TYPE_VALUES`).
|
|
62
|
+
- `ObjectIdKey` includes `assignmentId`.
|
|
63
|
+
|
|
64
|
+
**Migration**
|
|
65
|
+
|
|
66
|
+
1. `scoutProfiles`: for each document set `timezone = availability.tz`, `availability = availability.windows`,
|
|
67
|
+
`platformAccess = { all: false, platforms, special: [] }`, `ratingSummary = { avg: rating ?? null, count: rating ? 1 : 0 }`,
|
|
68
|
+
`status = 'active'`, `email` = the Clerk user's primary email (lowercased); unset `platforms` and `rating`.
|
|
69
|
+
2. `assignments`: drop the index on `{ taskId: 1 }` with `partialFilterExpression { status: 'active', deletedAt: null }`
|
|
70
|
+
and the index `{ assigneeUserId: 1, status: 1 }`, then `syncIndexes()`.
|
|
71
|
+
3. Seed `assigneeLoads` from active assignments: for scouts `{ userId, scope: 'global', active }`, for analysts
|
|
72
|
+
`{ userId, scope: orgId, active }`.
|
|
73
|
+
4. `orgSettings` and `analystProfiles` are created lazily by hero-service (Clerk webhooks); no backfill needed.
|
|
74
|
+
|
|
75
|
+
### Minor Changes
|
|
76
|
+
|
|
77
|
+
- d6e348c: Add the `Database` entity: one per organization, holding the `name` and `connectionString` of the org's Mongo
|
|
78
|
+
database (where `sources`, `dataItems`, … live). Ships `DatabaseSchema` / `DatabaseCreateSchema` /
|
|
79
|
+
`DatabaseUpdateSchema`, the `DatabaseStatus` and `DatabaseCollection` enums, `COLLECTIONS.databases`, the
|
|
80
|
+
`databaseSchema` Mongoose schema with a unique partial index on `orgId`, and the `Database` model in
|
|
81
|
+
`registerModels()`.
|
|
82
|
+
- Add a server-managed `databaseId` to `Source` and `DataItem` (new `DatabaseId` scalar in `common`). All
|
|
83
|
+
organizations' collected data lives in the shared service database, partitioned by `(orgId, databaseId)`;
|
|
84
|
+
today `databaseId` equals the Clerk org id, and it becomes the id of a physical `Database` record once orgs are
|
|
85
|
+
split into their own databases. `databaseId` is omitted from the `*CreateSchema`s and listed in
|
|
86
|
+
`SERVER_MANAGED_KEYS`; the `sources` and `dataItems` collections gain an `{ orgId, databaseId }` index. No
|
|
87
|
+
`Database` records are created any more — the entity is kept for the future split.
|
|
88
|
+
- 404cbc3: Initial release (0.1.0) of the shared Harvester V2 schema package.
|
|
89
|
+
|
|
90
|
+
- Browser-safe entry `.`: Zod schemas, inferred types, enums, `COLLECTIONS`, pagination and `ApiError` helpers.
|
|
91
|
+
- Server entry `./mongo`: Mongoose schemas typed against the Zod types, index definitions, `registerModels(connection)`,
|
|
92
|
+
`incrementTaskVersion` hook and the `objectIdsToStrings` `toJSON` transform.
|
|
93
|
+
- Entities: Mission, Task, Assignment, HoursLog, ScoutProfile, Source, SourceImport, DataItem, Scraper, ScraperMetric,
|
|
94
|
+
Deployment, AuditLog. Identity (users, orgs, memberships, roles) is owned by Clerk and referenced by id strings only.
|
|
95
|
+
|
|
96
|
+
### Patch Changes
|
|
97
|
+
|
|
98
|
+
- d6e348c: Extend the `Platform` enum (`PLATFORM_VALUES`) with `instagram`, `youtube`, `whatsapp`, `reddit`, `discord`,
|
|
99
|
+
`linkedin`, `threads`, `vk`, `snapchat` and `web` (open web: sites, forums, blogs). Existing values are unchanged.
|
|
100
|
+
|
|
101
|
+
Release notes are generated from `.changeset/` by `changeset version`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,276 @@
|
|
|
1
|
-
#
|
|
1
|
+
# badger_hq
|
|
2
2
|
|
|
3
|
-
|
|
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_hq` | Zod schemas, inferred TS types, enums, `COLLECTIONS`, pagination / `ApiError` helpers | Browser **and** Node |
|
|
11
|
+
| `./mongo` | `badger_hq/mongo` | Mongoose schemas, index definitions, `registerModels(connection)` | Node only |
|
|
12
|
+
|
|
13
|
+
Nothing reachable from `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_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_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_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_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_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_hq/mongo';
|
|
143
|
+
import { MissionCreateSchema, MissionSchema, type Mission } from '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
|
+
|
|
209
|
+
## Versioning
|
|
210
|
+
|
|
211
|
+
Semver, managed with [Changesets](https://github.com/changesets/changesets):
|
|
212
|
+
|
|
213
|
+
| Change | Bump |
|
|
214
|
+
|---|---|
|
|
215
|
+
| Add an optional field, add an enum value, add an index | `minor` |
|
|
216
|
+
| Remove or rename a field, change or remove an enum value, make an optional field required | `major` + a **Migration** note in the changeset |
|
|
217
|
+
| Docs, internal refactors, tightened validation with no shape change | `patch` |
|
|
218
|
+
|
|
219
|
+
Snapshot tests of the Zod shapes, Mongoose paths, enum values and index definitions fail the build on any unannounced
|
|
220
|
+
shape change; update the snapshot in the same PR as the changeset.
|
|
221
|
+
|
|
222
|
+
### Release flow
|
|
223
|
+
|
|
224
|
+
1. Open a PR with your change and `npm run changeset`.
|
|
225
|
+
2. CI (`.github/workflows/ci.yml`) runs lint, typecheck, tests and build.
|
|
226
|
+
3. On merge to `main`, `.github/workflows/release.yml` opens a *Version Packages* PR; merging it publishes to
|
|
227
|
+
GitHub Packages.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
npm install
|
|
233
|
+
npm test # vitest (incl. mongodb-memory-server integration tests)
|
|
234
|
+
npm run lint
|
|
235
|
+
npm run typecheck
|
|
236
|
+
npm run build # tsup -> dist/ (ESM + CJS + .d.ts for both entry points)
|
|
237
|
+
npm pack --dry-run
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Decisions
|
|
241
|
+
|
|
242
|
+
Choices made where the spec left room, kept consistent with the rest of it:
|
|
243
|
+
|
|
244
|
+
1. **Partial filter for soft-delete uniqueness.** MongoDB rejects `{ $exists: false }` in `partialFilterExpression`
|
|
245
|
+
(only `$exists: true`, equality, ranges, `$type`, `$and`/`$or`/`$in` are allowed). The equivalent supported form is
|
|
246
|
+
`{ deletedAt: null }`, which matches documents where `deletedAt` is missing or `null`. Used on `sources`,
|
|
247
|
+
`scoutProfiles`, `analystProfiles` and (combined with `status: 'active'`) `assignments`. Verified by integration test.
|
|
248
|
+
2. **Server-set fields omitted from Create inputs**, per entity: Mission `createdBy`; Task `approvedBy/At`, `doneBy/At`,
|
|
249
|
+
`version`; Assignment `assignedBy`, `activatedAt`, `endedAt`, `revokedBy`; HoursLog `scoutUserId`; ScoutProfile
|
|
250
|
+
`userId`, `status`, `ratingSummary`, `systemRecommendation.updatedBy/At`; Source `discoveredBy`, `lastCollectedAt`;
|
|
251
|
+
SourceImport `status`, `summary`, `rowErrors`,
|
|
252
|
+
`createdBy`, `committedAt`; Scraper `sourceCount`, `status`, `deploymentId`; Deployment `status`, `steps`,
|
|
253
|
+
`requestedBy`; AuditLog `actorUserId`, `at`. Status transitions (approve, mark done, vet, revoke) are expected to be
|
|
254
|
+
dedicated endpoints rather than generic `PATCH`es.
|
|
255
|
+
3. **Defaults** live on the base schemas so they apply to both parsing and inputs: statuses default to their initial
|
|
256
|
+
value (`draft`, `active`, `pending`, `validating`, `deploying`, `queued`), array fields default to `[]`,
|
|
257
|
+
`Task.version` to `1`, `TaskCriteria.emergency` and `AuditLog.actingAsSystem` to `false`.
|
|
258
|
+
4. **`ScoutProfile.taskLimit`** is optional (absent ⇒ global default). The default is exported as
|
|
259
|
+
`DEFAULT_SCOUT_TASK_LIMIT = 3` instead of storing `null`; analysts fall back to
|
|
260
|
+
`OrgSettings.defaultAnalystTaskLimit` (`DEFAULT_ANALYST_TASK_LIMIT = 5`).
|
|
261
|
+
5. **`Assignment.scoreBreakdown` / `AuditLog.diff`** are `Schema.Types.Mixed` in Mongoose (free-form), validated by Zod
|
|
262
|
+
as `Record<string, number>` / `unknown`.
|
|
263
|
+
6. **Cross-org indexes.** `assignments { assigneeUserId, status, orgId }`, `auditLogs { actorUserId, at }`, the unique
|
|
264
|
+
`scraperMetrics { scraperId, hour }` and everything on the global `scoutProfiles`, `scoutRatings` and
|
|
265
|
+
`assigneeLoads` collections intentionally don't start with `orgId`: they are per-user / per-scraper lookups.
|
|
266
|
+
All other compound indexes start with `orgId`.
|
|
267
|
+
7. **`ScraperMetric.errors`** is a reserved Mongoose pathname; the spec names the field so it is kept, with
|
|
268
|
+
`suppressReservedKeysWarning` set on that schema.
|
|
269
|
+
8. **Mongoose schema export names** are lowerCamelCase (`missionSchema`) to avoid colliding with the Zod
|
|
270
|
+
`MissionSchema` when both entry points are imported in one file.
|
|
271
|
+
9. **Package name** is `badger_hq` (set in `package.json`), published publicly to the npm registry.
|
|
272
|
+
10. **Multiple assignees per task (1.0.0).** The "one active assignee per task" unique index was replaced by
|
|
273
|
+
`{ taskId, assigneeUserId }` (partial, `status: 'active'`). Limits are enforced by `assigneeLoads`, not by an index,
|
|
274
|
+
because a scout's limit spans organizations while an analyst's is per org.
|
|
275
|
+
11. **`ScoutRating.score`** is a Zod literal union `1 | 2 | 3 | 4 | 5` (and a Mongoose `Number` enum) rather than
|
|
276
|
+
`int().min(1).max(5)`, so the type itself documents the scale.
|