@promptowl/contextnest-community 1.23.0 → 1.24.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/API.md +2092 -0
- package/CONFIGURATION.md +1 -0
- package/README.md +61 -9
- package/STEWARDSHIP.md +229 -0
- package/dist/{chunk-2YPY2HGZ.js → chunk-2TPQTN4Y.js} +5 -4
- package/dist/{chunk-MLQU4I5J.js → chunk-5EZOPA47.js} +5 -5
- package/dist/{chunk-UOMW7TT6.js → chunk-I3CSD6CK.js} +1 -1
- package/dist/{chunk-QLXC6542.js → chunk-KIAAEHWL.js} +49 -25
- package/dist/{chunk-QWNXWRWO.js → chunk-LA3VTQ22.js} +25 -1
- package/dist/{chunk-XYD6V2LH.js → chunk-XUIWAWDO.js} +28 -33
- package/dist/{chunk-5CVZMHHB.js → chunk-ZTT4U4NE.js} +2 -2
- package/dist/{client-LWDMNKX3.js → client-KEY4PYJH.js} +1 -1
- package/dist/{engine-QFF2IA3G.js → engine-S3QBQ7LH.js} +2 -2
- package/dist/{external-edit-service-F7D3VSA2.js → external-edit-service-6FNFPOJJ.js} +3 -3
- package/dist/{grants-service-TBGTCO5K.js → grants-service-UT3EQ3R7.js} +2 -2
- package/dist/index.js +371 -90
- package/dist/{migrations.postgres-APCVSYUE.js → migrations.postgres-AIQ7WSU7.js} +45 -4
- package/dist/{review-service-A7PNLWG5.js → review-service-HHGOTO6P.js} +6 -6
- package/dist/{stewardship-service-32UHIRQ5.js → stewardship-service-4DHNRULG.js} +3 -3
- package/dist/{version-service-RJ6CWZ3O.js → version-service-KIOQME6U.js} +3 -3
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
- package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
- package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
- package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
- package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
- package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
- package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
- package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
- package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
- package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
- package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
- package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
- package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
- package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
- package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
- package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
- package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
- package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
- package/dist/web3/assets/index-BM-h3DwI.css +1 -0
- package/dist/web3/assets/index-C1wTSjfP.js +29 -0
- package/dist/web3/assets/index-EaX2yql0.js +389 -0
- package/dist/web3/assets/page-B3yvQvHy.js +1 -0
- package/dist/web3/assets/page-BExMvttJ.js +1 -0
- package/dist/web3/assets/page-BUOADv2X.js +1 -0
- package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
- package/dist/web3/assets/page-Bicf02Cz.js +1 -0
- package/dist/web3/assets/page-BsPMDm5d.js +45 -0
- package/dist/web3/assets/page-ByyVLDVo.js +16 -0
- package/dist/web3/assets/page-CC0NJi8v.js +1 -0
- package/dist/web3/assets/page-CHb0F5hV.js +24 -0
- package/dist/web3/assets/page-CPjWQNKx.js +11 -0
- package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
- package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
- package/dist/web3/assets/page-CtaW65El.js +1 -0
- package/dist/web3/assets/page-DGOL9l8i.js +6 -0
- package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
- package/dist/web3/assets/play-C5YNKpAH.js +6 -0
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
- package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
- package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
- package/dist/web3/assets/select-DBFTrUmI.js +6 -0
- package/dist/web3/assets/send-0CHvmluT.js +6 -0
- package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
- package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
- package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
- package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
- package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
- package/dist/web3/assets/x-By7piikG.js +6 -0
- package/dist/web3/assets/zap-C2T8riBp.js +11 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -2
- package/dist/web3/assets/index-BJ-LRNis.js +0 -1382
- package/dist/web3/assets/index-DPMEt-A_.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -80,6 +80,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
80
80
|
| `POSTHOG_KEY` | `""` | PostHog project API key for product analytics in the UI. Empty = analytics off (the default — a self-hosted install brings its own project). Served to the browser via `/health`, but only while `TELEMETRY_ENABLED` is on, so that switch turns off everything this server sends outward. Also editable from Settings → Advanced. |
|
|
81
81
|
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Settings → Advanced. |
|
|
82
82
|
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
83
|
+
| `DRIFT_SCAN_INTERVAL_MS` | `30000` (30 s) | How often the drift scanner walks every nest for files edited outside the app (external edits) and stages them as suggestions for review. Set `0` to disable — do this on a GCS FUSE or other network mount, where the walk is slow and costly. Unset, empty or non-numeric falls back to the default. |
|
|
83
84
|
| `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
|
|
84
85
|
| `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
|
|
85
86
|
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
|
package/README.md
CHANGED
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
## What it is
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A shared knowledge base for your team **and** your AI agents, running on your own machine or infrastructure. Documents live in **nests**; every save is a version; in a stewarded nest a change waits for a reviewer before agents can read it. People write in the browser, agents read and write the same documents over MCP, HTTP or the `ctx` CLI.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- Import an existing folder or vault of markdown files in one step
|
|
15
|
-
- Export a nest as a portable bundle and re-import it on another self-hosted host
|
|
16
|
-
- Apply stewardship workflows — draft, pending review, approved
|
|
17
|
-
- Share nests with collaborators or publish them read-only to the public
|
|
18
|
-
- Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`, or register the nest in the ctx CLI (`ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`)
|
|
19
|
-
- Sync with the PromptOwl hosted platform for multi-user collaboration
|
|
13
|
+
Concretely, it lets you:
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
- Write Markdown documents — plus hosted HTML artifacts and CSV tables agents can query row by row — filed in folders, linked with `[[Title]]`, tagged, commented on, and versioned
|
|
16
|
+
- Import an existing folder of Markdown in one step; export a nest as a portable bundle
|
|
17
|
+
- Govern changes — draft → pending review → approved — with stewards per nest, tag or document, and a cross-nest inbox (My Work, My Drafts) for what is waiting on you
|
|
18
|
+
- Share a nest with people or teams, share a single document or folder, or publish read-only to the public
|
|
19
|
+
- Give agents deterministic reads: the same selector returns the same context every time, with a trace of what was pulled
|
|
20
|
+
- Run agents against a nest (Workflows, Beta): an agent is a document whose body is instructions; its output lands as a pending review
|
|
21
|
+
- Sync with the PromptOwl hosted platform for accounts, licensing and teams
|
|
22
|
+
|
|
23
|
+
Your PromptOwl account handles authentication, entitlement, and governance metadata.
|
|
22
24
|
|
|
23
25
|
## Quickstart
|
|
24
26
|
|
|
@@ -73,6 +75,40 @@ The key is read at boot. The server validates against PromptOwl on startup; if v
|
|
|
73
75
|
|
|
74
76
|
For redistribution, hosted-service, OEM, or regulated-industry licensing, contact **hoot@promptowl.ai**.
|
|
75
77
|
|
|
78
|
+
## First five minutes
|
|
79
|
+
|
|
80
|
+
1. **Create a nest** on the Nests page — or **Import folder** to bring in Markdown you already have.
|
|
81
|
+
2. **Write a document.** `[[Title]]` links another document, `@name` mentions a person, tags go under the title. Every save is a version.
|
|
82
|
+
3. **Turn on stewardship** (nest Settings) if you want review. Saves become drafts; **Submit for review**; a steward approves or rejects. Alone? Allow self-approve and you get the history without the ceremony.
|
|
83
|
+
4. **Connect an agent.** Inside a nest, **Connect** (bottom of the sidebar) gives you the MCP URL and a REST snippet; **Add with AI** (nest overview) is a prompt that has an agent write context in. Mint a key under Workspace → API keys.
|
|
84
|
+
5. **Share.** The whole nest with a person or team, one document or folder, or make it public with a read-only Reader mode.
|
|
85
|
+
|
|
86
|
+
The in-app **How it works** page (sidebar → Help) is the full tour; **Docs** (header) is the agent manual.
|
|
87
|
+
|
|
88
|
+
## Around the app
|
|
89
|
+
|
|
90
|
+
One sidebar, grouped:
|
|
91
|
+
|
|
92
|
+
| Group | Pages | Who |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| — | **Nests** — every nest you can see, pinned first | everyone |
|
|
95
|
+
| Work | **My Work** (reviews and handoffs waiting on you, across nests) · **My Drafts** (yours, never submitted) | everyone |
|
|
96
|
+
| Workspace | **Teams** · **API keys** | everyone |
|
|
97
|
+
| Admin | **Teammates** · **Server settings** · **Activity trace** | server admins |
|
|
98
|
+
| Help | **How it works** | everyone |
|
|
99
|
+
|
|
100
|
+
Inside a nest: the document tree, plus **Board** (documents as cards by folder, status or tag), **Graph** (documents, links, tags, stewards), **Definitions** (the nest's glossary) and **Workflows** (Beta). The user menu (top right) holds **Account** (name, password), keyboard shortcuts and logout. `Ctrl`/`⌘` `K` searches every nest and document; `?` lists shortcuts.
|
|
101
|
+
|
|
102
|
+
## Connect an agent
|
|
103
|
+
|
|
104
|
+
Point any agent at `<server>/llms.txt` — it is the complete manual (endpoints, MCP tools, selector grammar). The short version:
|
|
105
|
+
|
|
106
|
+
- **MCP** — `<server>/mcp` for everything the key can read, `<server>/nests/<id>/mcp` for one nest with the full toolset. Claude Code, Cursor and VS Code connect over HTTP with `Authorization: Bearer cnst_…`; Claude Desktop goes through `npx -y mcp-remote`.
|
|
107
|
+
- **REST** — `POST /nests/<id>/context` with a selector (`#tag`, `[[Title]]`, `type:document`, combined with `+ | -`) returns assembled context plus a trace. Full reference: [API.md](./API.md).
|
|
108
|
+
- **ctx CLI** — `ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`.
|
|
109
|
+
|
|
110
|
+
Keys are minted in the app (Workspace → API keys, or the Connect dialog) — one per client, user-wide or scoped to a nest. Servers in `AUTH_MODE=open` need none.
|
|
111
|
+
|
|
76
112
|
## System requirements
|
|
77
113
|
|
|
78
114
|
- **Node.js** 20.x or later
|
|
@@ -105,6 +141,14 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
|
|
|
105
141
|
| MCP server for AI agents | ✅ | ✅ |
|
|
106
142
|
| One-shot agent setup — generated connect prompt + `.env` download | ✅ | ✅ |
|
|
107
143
|
| Several API keys per account — one credential per client, rotate one at a time | ✅ | ✅ |
|
|
144
|
+
| Comments on documents — anchored threads, resolve / reopen, same threads over MCP | ✅ | ✅ |
|
|
145
|
+
| Table nodes — CSV facts with deterministic row queries (`POST /table-query`, `context_table_query`) | ✅ | ✅ |
|
|
146
|
+
| Teams — share a nest with a group once; import rosters from PromptOwl | ✅ | ✅ |
|
|
147
|
+
| Cross-nest inbox — My Work (reviews, handoffs) and My Drafts | ✅ | ✅ |
|
|
148
|
+
| Nest views — Board (kanban by folder / status / tag), Graph, Definitions glossary | ✅ | ✅ |
|
|
149
|
+
| Workflow plane (Beta) — agents as documents, runs, schedules, inbound hooks, Slack / Teams / webhook connectors | ✅ | ✅ |
|
|
150
|
+
| Activity trace — every governance action, per nest and server-wide | ✅ | ✅ |
|
|
151
|
+
| Notifications — Slack, Microsoft Teams, email | ✅ | ✅ |
|
|
108
152
|
| Centralized multi-tenant admin console | — | ✅ |
|
|
109
153
|
| Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
|
|
110
154
|
| SAML / SCIM provisioning | — | ✅ |
|
|
@@ -118,6 +162,14 @@ For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <htt
|
|
|
118
162
|
|
|
119
163
|
Release notes live in [CHANGELOG.md](./CHANGELOG.md).
|
|
120
164
|
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
- [CONFIGURATION.md](./CONFIGURATION.md) — every environment variable (port, auth mode, storage, notifications, telemetry)
|
|
168
|
+
- [API.md](./API.md) — complete REST reference with request and response bodies
|
|
169
|
+
- [STEWARDSHIP.md](./STEWARDSHIP.md) — the governance model: modes, stewards, scopes, prime documents, teams, super-admins
|
|
170
|
+
- `<server>/llms.txt` — the agent manual the running server publishes; rendered as **Docs** in the app
|
|
171
|
+
- **How it works** inside the app — a page-by-page tour
|
|
172
|
+
|
|
121
173
|
## Licensing
|
|
122
174
|
|
|
123
175
|
ContextNest Community Edition is **commercial software**. It is **not open source**.
|
package/STEWARDSHIP.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Stewardship — Quick Guide
|
|
2
|
+
|
|
3
|
+
A short, practical guide to the governance model the community server implements. The deep strategy lives in `docs/contextnest-whitepaper.md`; this is the "how does this actually work in the product" version.
|
|
4
|
+
|
|
5
|
+
## The two modes
|
|
6
|
+
|
|
7
|
+
Every nest is either **ungoverned** or **governed**, controlled by the `stewardship_enabled` flag on the nest.
|
|
8
|
+
|
|
9
|
+
| | Ungoverned (default) | Governed |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| New documents | Auto-approved, immediately available to AI | Start as **drafts**; need steward approval |
|
|
12
|
+
| Draft / pending / rejected lifecycle | Not shown | Shown, filterable |
|
|
13
|
+
| Review queue | Hidden | Visible, orderable |
|
|
14
|
+
| Stewards | Not shown | Assignable by scope |
|
|
15
|
+
| Reads gated by permission | No | Yes (in key mode) |
|
|
16
|
+
|
|
17
|
+
Flip the flag any time via the nest's **Settings → Stewardship**. Existing approved docs stay approved; existing drafts remain drafts.
|
|
18
|
+
|
|
19
|
+
## What a steward is
|
|
20
|
+
|
|
21
|
+
A **steward** is a person (identified by email) who governs a subset of the nest. Stewards have one of three roles:
|
|
22
|
+
|
|
23
|
+
| Role | Can read | Can edit | Can approve / reject |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| **Viewer** | yes | no | no |
|
|
26
|
+
| **Editor** | yes | yes | no |
|
|
27
|
+
| **Reviewer** | yes | yes | **yes** |
|
|
28
|
+
|
|
29
|
+
> **Authors can't approve their own work.** Even a reviewer-level steward can't approve a version they themselves authored. Separation of duties, enforced server-side.
|
|
30
|
+
|
|
31
|
+
## Scope — who governs what
|
|
32
|
+
|
|
33
|
+
Stewards are assigned at one of three scopes. When a document needs a steward (to approve, or to check access), the server resolves them in this order — **first match wins**:
|
|
34
|
+
|
|
35
|
+
| Priority | Scope | Target | Example |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| 1 | **Document** | Exact node id | `nodes/pricing-policy` — only this one doc |
|
|
38
|
+
| 2 | **Tag** | Tag name (lowercased, no `#`) | `security` — any doc tagged `#security` |
|
|
39
|
+
| 3 | **Nest** | (none — applies to the whole nest) | The fallback for anything not covered above |
|
|
40
|
+
|
|
41
|
+
If nothing matches and no steward resolves, the **nest owner** is the implicit steward (owner fallback). That keeps a freshly-enabled nest workable even with zero stewards configured.
|
|
42
|
+
|
|
43
|
+
## The approval flow
|
|
44
|
+
|
|
45
|
+
1. **Author** creates or edits a doc. In governed mode it saves as a **draft**.
|
|
46
|
+
2. **Author** clicks **Submit for Review**. The doc becomes **pending** and appears in the review queue for anyone whose scope resolves to that doc — and in their **My Work** page across nests. Until someone acts the author can withdraw it back to draft.
|
|
47
|
+
3. A **Reviewer steward** opens the review queue (or My Work), reads the doc, clicks **Approve** or **Reject**. Rejecting requires a note.
|
|
48
|
+
4. On approve, the doc becomes **approved** and is now AI-readable. The approved version number is pinned (`approvedVersion`), so subsequent drafts don't automatically replace it — AI keeps getting the last blessed version until a new one is approved.
|
|
49
|
+
|
|
50
|
+
### Auto-publish owner & admin edits (`allow_self_approve`)
|
|
51
|
+
|
|
52
|
+
A per-nest flag (**off** by default; nest Settings → "Auto-publish owner & admin edits"). When it's **on**, a create or edit by the nest **owner** or an **admin** publishes immediately — it skips steps 2–4 entirely instead of landing as a draft. This is a governance *bypass* for the two roles that already hold approval authority, not a separation-of-duties exception. **Editor and reviewer flows are unchanged**: their edits still save as drafts and go through Submit → Review → Approve regardless of the flag. The pending-review edit lock still applies, so an owner/admin can't overwrite a doc a teammate has in review. Turning the flag back off doesn't unpublish anything; it only affects future edits.
|
|
53
|
+
|
|
54
|
+
## Prime documents
|
|
55
|
+
|
|
56
|
+
Most content in a nest doesn't need a review gate — call notes, social drafts, scratch research. A small set does: messaging architecture, the sales playbook, pricing policy. **Prime** marks that second set.
|
|
57
|
+
|
|
58
|
+
A prime document **always** goes through draft → pending → approved, no matter who writes it. It is the one thing `allow_self_approve` does not bypass: an owner editing a prime document still lands a draft.
|
|
59
|
+
|
|
60
|
+
### How a document becomes prime
|
|
61
|
+
|
|
62
|
+
Resolution mirrors the steward scope order — **document → tag → nest**, first match wins:
|
|
63
|
+
|
|
64
|
+
| Priority | Scope | How it's set |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| 1 | **Document** | The Prime switch in the document editor (owner/admin only). `true` forces prime; `false` **exempts** this one doc from an otherwise-prime tag. |
|
|
67
|
+
| 2 | **Tag** | The doc carries one of the nest's **prime tags** (Settings → Prime tags; `prime-document` out of the box). |
|
|
68
|
+
| 3 | **Nest** | Nothing matched → not prime. |
|
|
69
|
+
|
|
70
|
+
Only the nest owner or an admin can set or clear the document flag — it's a governance decision, not an authoring one. Otherwise any editor could un-flag the playbook and walk their own edit past the gate.
|
|
71
|
+
|
|
72
|
+
### Reviewing prime documents only (`prime_only_review`)
|
|
73
|
+
|
|
74
|
+
A per-nest flag (**off** by default; nest Settings → "Review prime documents only"). It changes what a governed nest's *default* is:
|
|
75
|
+
|
|
76
|
+
| | `prime_only_review` off (default) | on |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Ordinary document, any author | Draft → review → approve (unless the author has a self-approve bypass) | **Publishes immediately** |
|
|
79
|
+
| Prime document, any author | Draft → review → approve | Draft → review → approve |
|
|
80
|
+
|
|
81
|
+
That's the "most content self-publishes, high-stakes content is gated" posture. Leaving it off preserves the original behaviour, so turning stewardship on doesn't silently change meaning for an existing nest.
|
|
82
|
+
|
|
83
|
+
While `prime_only_review` is on, `allow_self_approve` has no effect at all — non-prime documents already publish for everyone, and prime documents ignore the bypass. Settings disables that switch and says so rather than leaving a live control that changes nothing.
|
|
84
|
+
|
|
85
|
+
Prime only applies to **governed** nests. With stewardship off there are no stewards to approve anything, so a prime tag is inert until you turn stewardship on.
|
|
86
|
+
|
|
87
|
+
### At a glance
|
|
88
|
+
|
|
89
|
+
Prime documents carry a **Prime** badge in the nest's document list, with a tooltip saying whether it came from the document flag or an inherited tag. To see every document a given prime tag covers, filter the list by that tag.
|
|
90
|
+
|
|
91
|
+
Flipping any of this only affects **future** writes. Marking a published document prime doesn't unpublish it; the next edit is what needs approval.
|
|
92
|
+
|
|
93
|
+
## Who sees what
|
|
94
|
+
|
|
95
|
+
Governance gates two things: who can **approve**, and who can **read**.
|
|
96
|
+
|
|
97
|
+
- **Approve gate**: server-side, always on when stewardship is enabled. Non-stewards can't approve.
|
|
98
|
+
- **Read gate**: only active in **key mode** (`AUTH_MODE=key`, real user accounts with API keys). In **open mode** — which is the default for a solo/local server — everyone is the same anonymous admin, so read-gating is a no-op.
|
|
99
|
+
|
|
100
|
+
In key mode, when stewardship is enabled on a nest:
|
|
101
|
+
- Node list / single-read / search / query responses are filtered to docs the caller can access
|
|
102
|
+
- A non-steward gets an empty list or a 403 on the node they asked for
|
|
103
|
+
- The nest **owner** and any resolved **steward (any role)** can read; **super admins** bypass the check
|
|
104
|
+
|
|
105
|
+
## Server super-admins
|
|
106
|
+
|
|
107
|
+
Super-admins administer **every** nest on the server without being added per-nest. The effective set is the union of three sources:
|
|
108
|
+
|
|
109
|
+
- **license** — the PromptOwl account that owns the installed license (always; never revocable)
|
|
110
|
+
- **config** — emails in `access.yaml: super_admins` (the bootstrap; edit the file + restart to change)
|
|
111
|
+
- **granted** — grants managed from **Teammates → Superadmins** in the UI, or `GET/POST /admin/super-admins` (see `API.md`)
|
|
112
|
+
|
|
113
|
+
A super-admin can:
|
|
114
|
+
|
|
115
|
+
- Read every document (bypasses the read gate, even with stewardship enabled)
|
|
116
|
+
- Change a nest's visibility (private / org / public)
|
|
117
|
+
- Add, remove, and re-role collaborators
|
|
118
|
+
- Add, remove, and re-role stewards
|
|
119
|
+
- Approve and reject pending versions in any nest's review queue
|
|
120
|
+
- Manage the official community sites trusted for SSO auto-login (**Server settings → Community sites**, or `GET/POST /admin/community-sites` — see `API.md`)
|
|
121
|
+
|
|
122
|
+
What a super-admin **cannot** do (owner-only operations):
|
|
123
|
+
|
|
124
|
+
- Delete a nest
|
|
125
|
+
- Transfer ownership
|
|
126
|
+
|
|
127
|
+
Use this for a small set of platform operators — a CEO, a head of governance, an internal compliance admin — not as a default broad-access mechanism. Day-to-day governance should still flow through per-nest stewards.
|
|
128
|
+
|
|
129
|
+
## Teams — sharing with a group
|
|
130
|
+
|
|
131
|
+
Instead of adding people one at a time, you can create a **team** — a reusable,
|
|
132
|
+
owner-managed group — and share it onto a nest. Each team member carries a
|
|
133
|
+
**role** set on the membership (not on the share), and that role applies on
|
|
134
|
+
**every** nest the team is shared to:
|
|
135
|
+
|
|
136
|
+
| Member role | Nest access | Governance |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| **viewer** | read | viewer |
|
|
139
|
+
| **editor** | write | editor |
|
|
140
|
+
| **admin** | admin | admin |
|
|
141
|
+
|
|
142
|
+
Key points:
|
|
143
|
+
|
|
144
|
+
- A member's role is uniform across nests — set it once on the team. To vary
|
|
145
|
+
access per nest, use a different team (or a direct collaborator/steward grant).
|
|
146
|
+
- When a user reaches a nest by several paths (a direct collaborator grant **and**
|
|
147
|
+
team membership), the **highest** access wins.
|
|
148
|
+
- Sharing a team whose members carry a governance role (viewer/editor)
|
|
149
|
+
**enables stewardship** on the nest — same effect as assigning an individual
|
|
150
|
+
steward. An `admin`-only team is collaborator-style and doesn't.
|
|
151
|
+
- Removing a member, deleting the team, or unsharing it **revokes access
|
|
152
|
+
immediately** — there are no per-user rows to clean up.
|
|
153
|
+
- Managing a team is gated by your role on it: editor/admin members (and the
|
|
154
|
+
owner) can add people, capped to their own level; admins can also change
|
|
155
|
+
roles and remove members; only the owner can delete the team.
|
|
156
|
+
|
|
157
|
+
### Importing a team from PromptOwl
|
|
158
|
+
|
|
159
|
+
If you already run teams in PromptOwl, the Teams page has a **PromptOwl Teams**
|
|
160
|
+
tab listing your teams there with their members. Each one has a **Sync** button
|
|
161
|
+
that copies it here as a normal team owned by you, instead of you retyping every
|
|
162
|
+
member. Members without an account here become invited placeholders, exactly as
|
|
163
|
+
if you'd added them by hand.
|
|
164
|
+
|
|
165
|
+
**The tab only appears if you signed in with PromptOwl** — that sign-in is what
|
|
166
|
+
gives this server permission to read your teams. Signing in another way (SSO,
|
|
167
|
+
your identity provider, or email and password) means no PromptOwl identity to
|
|
168
|
+
read teams for, so the tab isn't offered.
|
|
169
|
+
|
|
170
|
+
**You can import a team you're an owner or editor of in PromptOwl.** Teams you're
|
|
171
|
+
view-only on are listed for reference but can't be imported: the copy would be
|
|
172
|
+
yours to control here — rename it, rewrite its roster, share it onto a nest —
|
|
173
|
+
and that's more than PromptOwl lets you do with that team. Ask one of its owners
|
|
174
|
+
or editors to import it instead.
|
|
175
|
+
|
|
176
|
+
Already-imported teams are marked **Synced** in that tab, and show a **PromptOwl**
|
|
177
|
+
badge in the ContextNest Teams tab. Two things worth knowing before you sync
|
|
178
|
+
one again:
|
|
179
|
+
|
|
180
|
+
- **PromptOwl is the source of truth on every sync.** The roster is reconciled to
|
|
181
|
+
match: roles are reset, and anyone no longer in the PromptOwl team is removed —
|
|
182
|
+
including people you added to this team by hand here. If you need a group that
|
|
183
|
+
differs from PromptOwl, make it a separate team rather than editing an
|
|
184
|
+
imported one.
|
|
185
|
+
- **Your PromptOwl access isn't stored on this server.** The connection lives in
|
|
186
|
+
a cookie in your own browser and ends when your session does — this server's
|
|
187
|
+
database never holds anything that can reach your PromptOwl account, so a
|
|
188
|
+
stolen backup of it exposes nothing of yours. The connection is also limited
|
|
189
|
+
to reading your profile and your teams: it cannot chat as you or spend your
|
|
190
|
+
credits. Signing out ends it, and revoking the device in PromptOwl's account
|
|
191
|
+
settings ends it immediately from the other side.
|
|
192
|
+
|
|
193
|
+
PromptOwl's `User` role — its default membership — maps to **viewer** here, as
|
|
194
|
+
does any role this version doesn't recognize; `Owner` maps to admin, and
|
|
195
|
+
`Editor`/`Viewer` map across as themselves.
|
|
196
|
+
|
|
197
|
+
Members see the teams they belong to on the **Teams** page and any team-shared
|
|
198
|
+
nest on their dashboard. Manage teams there (create, add/remove members with a
|
|
199
|
+
role); share them from a nest's sharing panel. See `API.md` §3a for the endpoints.
|
|
200
|
+
|
|
201
|
+
## Picking a scope when you assign a steward
|
|
202
|
+
|
|
203
|
+
Rules of thumb:
|
|
204
|
+
|
|
205
|
+
- **Nest scope** — use sparingly. "This person reviews everything in this nest."
|
|
206
|
+
- **Tag scope** — the most common. "Legal reviews anything with `#legal`. Security reviews `#auth` and `#pii`."
|
|
207
|
+
- **Document scope** — override. "This specific doc has a dedicated reviewer regardless of its tags."
|
|
208
|
+
|
|
209
|
+
You can stack them: a doc tagged `#legal` can have a doc-level steward that overrides the tag steward. Priority order resolves ties.
|
|
210
|
+
|
|
211
|
+
## `stewards.yaml` (optional)
|
|
212
|
+
|
|
213
|
+
You can also declare stewards declaratively via a `stewards.yaml` file in the nest's vault directory and run `POST /nests/:id/stewards/sync`. Useful for version-controlled governance. Example:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
version: 1
|
|
217
|
+
nest:
|
|
218
|
+
- email: governance-lead@acme.com
|
|
219
|
+
role: reviewer
|
|
220
|
+
tags:
|
|
221
|
+
"#legal":
|
|
222
|
+
- email: legal@acme.com
|
|
223
|
+
role: reviewer
|
|
224
|
+
"#security":
|
|
225
|
+
- email: security-team@acme.com
|
|
226
|
+
role: reviewer
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Sync replaces all stewards for that nest from the file. Prefer the UI for day-to-day adds; prefer the file for reproducible setups and code review.
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
nestAllowsSelfApprove,
|
|
9
9
|
nestName,
|
|
10
10
|
primaryRole,
|
|
11
|
+
resolveNestAccess,
|
|
11
12
|
resolveNestPermission,
|
|
12
13
|
resolveTeamRolesForUser,
|
|
13
14
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-KIAAEHWL.js";
|
|
15
16
|
import {
|
|
16
17
|
ConflictError,
|
|
17
18
|
ValidationError
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
config,
|
|
21
22
|
getDb,
|
|
22
23
|
isEmailish
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-LA3VTQ22.js";
|
|
24
25
|
|
|
25
26
|
// src/governance/stewardship-service.ts
|
|
26
27
|
import { v4 as uuid } from "uuid";
|
|
@@ -441,8 +442,8 @@ async function resolveUserRoles(nestId, userEmail, opts) {
|
|
|
441
442
|
}
|
|
442
443
|
async function canManageStewards(nestId, userId) {
|
|
443
444
|
if (config.AUTH_MODE === "open") return true;
|
|
444
|
-
const
|
|
445
|
-
return
|
|
445
|
+
const { rawPermission } = await resolveNestAccess(nestId, userId);
|
|
446
|
+
return rawPermission === "owner" || rawPermission === "admin";
|
|
446
447
|
}
|
|
447
448
|
async function canCreateInNest(nestId, userEmail) {
|
|
448
449
|
if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
resolveNestWideRoles,
|
|
5
5
|
resolveStewardsForNode,
|
|
6
6
|
stewardCoverageForUser
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2TPQTN4Y.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-I3CSD6CK.js";
|
|
13
13
|
import {
|
|
14
14
|
createVersion,
|
|
15
15
|
getApprovedVersion,
|
|
16
16
|
getApprovedVersions,
|
|
17
17
|
getCurrentVersion,
|
|
18
18
|
setApprovedVersion
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ZTT4U4NE.js";
|
|
20
20
|
import {
|
|
21
21
|
buildDocContext,
|
|
22
22
|
buildTitleMap,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
resolveNestPermission,
|
|
38
38
|
sendEmailToRecipient,
|
|
39
39
|
titleForNode
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-KIAAEHWL.js";
|
|
41
41
|
import {
|
|
42
42
|
ConflictError,
|
|
43
43
|
NotFoundError,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
import {
|
|
47
47
|
config,
|
|
48
48
|
getDb
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-LA3VTQ22.js";
|
|
50
50
|
|
|
51
51
|
// src/governance/review-service.ts
|
|
52
52
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LA3VTQ22.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -1296,8 +1296,11 @@ async function listNotifications(userEmail, opts = {}) {
|
|
|
1296
1296
|
CASE WHEN dr.target_type = 'document' THEN dr.node_id END,
|
|
1297
1297
|
-- A mention names the document directly; there's no request row.
|
|
1298
1298
|
CASE WHEN n.kind = 'mention' THEN n.subject_id END
|
|
1299
|
-
) AS node_id
|
|
1299
|
+
) AS node_id,
|
|
1300
|
+
-- Name resolved here so the inbox never fetches the nest list for it.
|
|
1301
|
+
COALESCE(ns.name, n.nest_id) AS nest_name
|
|
1300
1302
|
FROM notifications n
|
|
1303
|
+
LEFT JOIN nests ns ON ns.id = n.nest_id
|
|
1301
1304
|
LEFT JOIN review_requests rr ON rr.id = n.subject_id
|
|
1302
1305
|
LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
|
|
1303
1306
|
WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
|
|
@@ -2488,6 +2491,9 @@ var FlatNestStorage = class extends NestStorage2 {
|
|
|
2488
2491
|
}
|
|
2489
2492
|
};
|
|
2490
2493
|
|
|
2494
|
+
// src/nodes/node-index.ts
|
|
2495
|
+
import { listSuggestions } from "@promptowl/contextnest-engine";
|
|
2496
|
+
|
|
2491
2497
|
// src/shared/node-id.ts
|
|
2492
2498
|
function assertSafeNodeId(rawId) {
|
|
2493
2499
|
let id = rawId;
|
|
@@ -2616,13 +2622,21 @@ async function ensureNodeIndex(nestId) {
|
|
|
2616
2622
|
return run;
|
|
2617
2623
|
}
|
|
2618
2624
|
async function rebuildNodeIndex(nestId) {
|
|
2619
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2620
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2625
|
+
const { engineCache: engineCache2 } = await import("./engine-S3QBQ7LH.js");
|
|
2626
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-6FNFPOJJ.js");
|
|
2621
2627
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2622
2628
|
dropDiscoveryCache();
|
|
2623
2629
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
2624
2630
|
const documents = await storage.discoverDocuments({ includeRetired: true });
|
|
2625
2631
|
const staged = await documentsWithSuggestions(nestId);
|
|
2632
|
+
const stagedMetas = (await Promise.all(
|
|
2633
|
+
[...staged].map(
|
|
2634
|
+
(id) => listSuggestions(storage, id).catch((err) => {
|
|
2635
|
+
console.error("[node-index] listSuggestions failed", nestId, id, err);
|
|
2636
|
+
return [];
|
|
2637
|
+
})
|
|
2638
|
+
)
|
|
2639
|
+
)).flat();
|
|
2626
2640
|
const db = getDb();
|
|
2627
2641
|
const insertSql = insertOrReplace(
|
|
2628
2642
|
db,
|
|
@@ -2630,18 +2644,15 @@ async function rebuildNodeIndex(nestId) {
|
|
|
2630
2644
|
["nest_id", "node_id"],
|
|
2631
2645
|
UPSERT_SET
|
|
2632
2646
|
);
|
|
2633
|
-
const stagedSql = insertOrIgnore(
|
|
2634
|
-
db,
|
|
2635
|
-
"INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
|
|
2636
|
-
);
|
|
2647
|
+
const stagedSql = insertOrIgnore(db, SUGGESTION_INSERT);
|
|
2637
2648
|
await db.transaction(async (tx) => {
|
|
2638
2649
|
await tx.run("DELETE FROM node_index WHERE nest_id = ?", [nestId]);
|
|
2639
2650
|
for (const doc of documents) {
|
|
2640
2651
|
await tx.run(insertSql, [nestId, ...rowValues(doc)]);
|
|
2641
2652
|
}
|
|
2642
2653
|
await tx.run("DELETE FROM node_suggestions WHERE nest_id = ?", [nestId]);
|
|
2643
|
-
for (const
|
|
2644
|
-
await tx.run(stagedSql,
|
|
2654
|
+
for (const m of stagedMetas) {
|
|
2655
|
+
await tx.run(stagedSql, suggestionValues(nestId, m));
|
|
2645
2656
|
}
|
|
2646
2657
|
if ((writeGeneration.get(nestId) ?? 0) !== startedAt) return;
|
|
2647
2658
|
await tx.run(
|
|
@@ -2651,30 +2662,42 @@ async function rebuildNodeIndex(nestId) {
|
|
|
2651
2662
|
});
|
|
2652
2663
|
return documents.length;
|
|
2653
2664
|
}
|
|
2654
|
-
|
|
2665
|
+
var SUGGESTION_INSERT = "INSERT INTO node_suggestions (nest_id, node_id, suggestion_id, source, actor, detected_at, target_hash, proposed_hash, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
2666
|
+
var suggestionValues = (nestId, m) => [
|
|
2667
|
+
nestId,
|
|
2668
|
+
m.document_id,
|
|
2669
|
+
m.suggestion_id,
|
|
2670
|
+
m.source,
|
|
2671
|
+
m.actor,
|
|
2672
|
+
m.detected_at,
|
|
2673
|
+
m.target_hash,
|
|
2674
|
+
m.proposed_hash,
|
|
2675
|
+
m.note ?? null
|
|
2676
|
+
];
|
|
2677
|
+
async function syncSuggestionRows(nestId, nodeId, metas) {
|
|
2655
2678
|
try {
|
|
2656
2679
|
const db = getDb();
|
|
2657
|
-
|
|
2658
|
-
|
|
2680
|
+
const insertSql = insertOrIgnore(db, SUGGESTION_INSERT);
|
|
2681
|
+
await db.transaction(async (tx) => {
|
|
2682
|
+
await tx.run(
|
|
2659
2683
|
"DELETE FROM node_suggestions WHERE nest_id = ? AND node_id = ?",
|
|
2660
2684
|
[nestId, nodeId]
|
|
2661
2685
|
);
|
|
2662
|
-
|
|
2663
|
-
}
|
|
2664
|
-
await db.run(
|
|
2665
|
-
insertOrIgnore(
|
|
2666
|
-
db,
|
|
2667
|
-
"INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
|
|
2668
|
-
),
|
|
2669
|
-
[nestId, nodeId]
|
|
2670
|
-
);
|
|
2686
|
+
for (const m of metas) await tx.run(insertSql, suggestionValues(nestId, m));
|
|
2687
|
+
});
|
|
2671
2688
|
} catch (err) {
|
|
2672
|
-
console.error("[node-index] suggestion
|
|
2689
|
+
console.error("[node-index] suggestion rows failed", nestId, nodeId, err);
|
|
2673
2690
|
}
|
|
2674
2691
|
}
|
|
2692
|
+
async function listSuggestionRows(nestId) {
|
|
2693
|
+
return await getDb().all(
|
|
2694
|
+
"SELECT * FROM node_suggestions WHERE nest_id = ? ORDER BY detected_at DESC",
|
|
2695
|
+
[nestId]
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2675
2698
|
async function nodesWithSuggestions(nestId) {
|
|
2676
2699
|
const rows = await getDb().all(
|
|
2677
|
-
"SELECT node_id FROM node_suggestions WHERE nest_id = ?",
|
|
2700
|
+
"SELECT DISTINCT node_id FROM node_suggestions WHERE nest_id = ?",
|
|
2678
2701
|
[nestId]
|
|
2679
2702
|
);
|
|
2680
2703
|
return new Set(rows.map((r) => r.node_id));
|
|
@@ -2916,7 +2939,8 @@ export {
|
|
|
2916
2939
|
markIndexStale,
|
|
2917
2940
|
ensureNodeIndex,
|
|
2918
2941
|
rebuildNodeIndex,
|
|
2919
|
-
|
|
2942
|
+
syncSuggestionRows,
|
|
2943
|
+
listSuggestionRows,
|
|
2920
2944
|
nodesWithSuggestions,
|
|
2921
2945
|
indexedNodeIds,
|
|
2922
2946
|
readIndexedNodes,
|
|
@@ -1757,6 +1757,30 @@ function runMigrations(db) {
|
|
|
1757
1757
|
recordMigration("042_community_sites");
|
|
1758
1758
|
})();
|
|
1759
1759
|
}
|
|
1760
|
+
if (!hasMigration("043_node_suggestion_details")) {
|
|
1761
|
+
db.transaction(() => {
|
|
1762
|
+
db.exec(`
|
|
1763
|
+
DROP TABLE IF EXISTS node_suggestions;
|
|
1764
|
+
CREATE TABLE node_suggestions (
|
|
1765
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1766
|
+
node_id TEXT NOT NULL,
|
|
1767
|
+
suggestion_id TEXT NOT NULL,
|
|
1768
|
+
source TEXT NOT NULL,
|
|
1769
|
+
actor TEXT NOT NULL,
|
|
1770
|
+
detected_at TEXT NOT NULL,
|
|
1771
|
+
target_hash TEXT NOT NULL,
|
|
1772
|
+
proposed_hash TEXT NOT NULL,
|
|
1773
|
+
note TEXT,
|
|
1774
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1775
|
+
PRIMARY KEY (nest_id, node_id, suggestion_id)
|
|
1776
|
+
);
|
|
1777
|
+
CREATE INDEX IF NOT EXISTS idx_node_suggestions_nest_detected
|
|
1778
|
+
ON node_suggestions(nest_id, detected_at DESC);
|
|
1779
|
+
UPDATE nests SET index_synced_at = NULL;
|
|
1780
|
+
`);
|
|
1781
|
+
recordMigration("043_node_suggestion_details");
|
|
1782
|
+
})();
|
|
1783
|
+
}
|
|
1760
1784
|
}
|
|
1761
1785
|
function fkViolationCounts(db) {
|
|
1762
1786
|
const rows = db.pragma("foreign_key_check");
|
|
@@ -2031,7 +2055,7 @@ async function initDb() {
|
|
|
2031
2055
|
if (config.DB_DRIVER === "postgres") {
|
|
2032
2056
|
const { Pool } = await import("pg");
|
|
2033
2057
|
const { PostgresAdapter } = await import("./adapter.postgres-6VZCMPQL.js");
|
|
2034
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
2058
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-AIQ7WSU7.js");
|
|
2035
2059
|
const pool = new Pool(buildPgConfig());
|
|
2036
2060
|
adapter = new PostgresAdapter(pool);
|
|
2037
2061
|
await runPostgresMigrations(adapter);
|