Proposal lifecycle (Hub)
This document defines states, roles, and identifiers for Knowtation Hub proposals. It is the reference for extending ProposalRecord (Node, canister, OpenAPI) without drift.
States
| Status | Meaning |
|---|---|
proposed |
Waiting for review; not applied to the canonical vault. |
approved |
Applied to the vault; proposal record kept for audit/history. |
discarded |
Rejected; not applied. May be bulk-discarded when notes are deleted (see HUB-METADATA-BULK-OPS.md). |
Allowed transitions:
proposed→approved(admin: Approve; may require human evaluation first — see Evaluation below)proposed→discarded(admin: Discard, or bulk housekeeping)
There is no draft status in the store today; agents create proposed rows via POST /api/v1/proposals.
Roles (Phase 13 + evaluator)
| Role | List / view proposals | Create proposal | Submit evaluation | Approve / discard |
|---|---|---|---|---|
viewer |
Yes | No | No | No |
editor |
Yes | Yes | No | No |
evaluator |
Yes | No | Yes | Only if HUB_EVALUATOR_MAY_APPROVE=1 (approve only; discard stays admin on Node Hub) |
admin |
Yes | Yes | Yes | Yes |
Optional Tier-2 enrichment (POST /api/v1/proposals/:id/enrich when KNOWTATION_HUB_PROPOSAL_ENRICH=1): editor or admin.
Optimistic concurrency: base_state_id
When a proposal targets an existing note path, the client may send base_state_id: a fingerprint of the vault note as the client last saw it (e.g. from GET /api/v1/notes/:path). On Approve, the Hub (self-hosted Node) recomputes the current fingerprint for that path and returns 409 CONFLICT if it does not match either the request body’s base_state_id (if provided) or the value stored on the proposal.
Format kn1_ (FNV-1a 64-bit)
- Prefix:
kn1_ - Suffix: 16 lowercase hex characters = FNV-1a 64-bit over UTF-8 bytes of
canonicalJSON(frontmatterObject) + "\0" + body canonicalJSONmeans sorted object keys at all levels (see lib/note-state-id.mjsstableStringify).
Absent path (new note; no file at that path yet): fingerprint is the FNV-1a of the single byte 0x00, still with prefix kn1_ (see absentNoteStateId()).
Hosted canister: Approve does not recompute kn1_ in Motoko today (frontmatter serialization may differ from Node). Clients should rely on Node Hub for strict checks, or treat base_state_id as advisory on canister-only flows until parity is implemented.
external_ref on approve (optional Muse bridge)
Besides setting external_ref at POST /api/v1/proposals (create), operators may set or resolve it at POST /api/v1/proposals/:id/approve: the client can send external_ref in the approve body, or the server may fill it from an optional Muse lineage callback when MUSE_URL is configured. Approve never fails because Muse is unreachable. See MUSE-THIN-BRIDGE.md.
Optional fields (augmentation)
| Field | Purpose |
|---|---|
intent |
Human- or agent-readable reason. |
external_ref |
Optional cross-system id (e.g. Muse lineage). |
labels |
String array for triage/filter (not only inside proposed frontmatter). |
source |
e.g. agent, human, import. |
suggested_labels, assistant_notes, assistant_model, assistant_at |
Tier-2 enrichment output when enabled. |
review_queue |
Optional string for triage (e.g. legal), set by deterministic triggers or client. |
review_severity |
standard or elevated, from triggers. |
auto_flag_reasons |
String array of structured reason codes (e.g. phrase:…, path_prefix:…); audited on create when non-empty. |
review_hints, review_hints_at, review_hints_model |
Optional async LLM text for humans only; never the sole merge gate. |
Deterministic review triggers
Org-configurable rules in data/hub_proposal_review_triggers.json (override; packaged default hub/proposal-review-triggers-default.json) can force evaluation_status: pending and set review_queue / review_severity from:
literal_phrases— case-insensitive substring match on path + body + intent (bounded list/size per lib/hub-proposal-review-triggers.mjs).path_prefixes— vault-relative path prefix match.label_any— intersection with proposallabels.
Hosted: The gateway merges the same logic into POST /api/v1/proposals before the canister (see lib/hub-proposal-create-augment.mjs).
Human evaluation (Phase: proposal evaluation)
Evaluation is a human-led record (who/when/outcome/checklist/comment, optional grade). It is not the same as Tier-2 Enrich (LLM assist). LLM output must not be the sole merge authority.
evaluation_status (orthogonal to status)
| Value | Meaning |
|---|---|
none |
No evaluation required for this proposal (gate off at creation), or legacy row with no field set. |
pending |
Evaluation expected before approve (gate on at creation). |
passed |
Evaluator recorded a pass outcome; approve allowed without waiver. |
failed |
Evaluator recorded fail; approve blocked unless admin supplies a waiver (see below). |
needs_changes |
Evaluator requested changes; approve blocked unless waiver. |
Transitions (human action via POST /api/v1/proposals/:id/evaluation):
none→passed|failed|needs_changes(optional audit when gate is off)pending→passed|failed|needs_changesfailed|needs_changes→passed|failed|needs_changes(re-evaluation after edits)
approved / discarded proposals cannot receive new evaluations.
Stored fields (Node hub_proposals.json and canister ProposalRecord)
| Field | Purpose |
|---|---|
evaluation_status |
One of the values above. |
evaluation_grade |
Optional (e.g. letter or 1–5). Secondary to pass/fail. |
evaluation_checklist |
JSON array: [{ "id", "label", "passed" }] merged from org rubric + submission. |
evaluation_comment |
Free text; required for outcomes failed and needs_changes. |
evaluated_by |
JWT sub of evaluator (v1: admin). |
evaluated_at |
ISO timestamp. |
evaluation_waiver |
Set on approve when bypassing a non-pass state: { "by", "at", "reason" }. |
Gate: require evaluation before approve
Policy resolution (lib/hub-proposal-policy.mjs):
HUB_PROPOSAL_EVALUATION_REQUIRED=1ortrue→ gate on for new proposals.=0orfalse→ gate off.- If unset: read
data/hub_proposal_policy.json; ifproposal_evaluation_required === true, gate on; else off.
When the gate is on, new proposals are created with evaluation_status: "pending" (unless triggers already implied pending). Approve is rejected with 403 / EVALUATION_REQUIRED unless:
evaluation_status === "passed", or- the approve request includes a non-empty
waiver_reason(trimmed length ≥ 3), which recordsevaluation_waiverand an audit entry (approve_waiver).
When the gate is off at create, new proposals use evaluation_status: "none" unless review triggers force pending; admins may approve without submitting evaluation, but may still submit evaluation for audit.
Hosted canister: The gateway injects evaluation_status: "pending" on create when policy is on (same resolution using repo data/ beside the gateway). Approve rules on the canister are unchanged. Canister stores review_queue, review_severity, auto_flag_reasons_json, and optional review_hints (V3 ProposalRecord); upgrade migrates existing rows.
Rubric
Default checklist items ship in-repo (hub/proposal-rubric-default.json). Override with data/hub_proposal_rubric.json (same { "items": [{ "id", "label" }] } shape). See PROPOSAL-EVALUATION-RUBRIC-DEFAULT.md.
Who evaluates
Admins and evaluator users may POST …/evaluation. Approve defaults to admin; set HUB_EVALUATOR_MAY_APPROVE=1 to allow evaluator to approve. Discard remains admin on the Node Hub.
Optional LLM review hints
When KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS=1, the Hub may asynchronously populate review_hints (self-hosted: after create; hosted: gateway schedules a canister POST …/review-hints after create). Prompt-injection: treat model output as untrusted; it does not change evaluation_status unless you add a separate policy (not shipped). Privacy: proposal body may be sent to OpenAI or Ollama per lib/llm-complete.mjs.
Related
- HUB-API.md §3.4 Proposals
- IMPORT-EVALS.md (retrieval vs proposal evaluation)
- AGENT-INTEGRATION.md §4 — proposals, metadata, optional Muse linkage
- HUB-API.md proposals section (lifecycle extensions)