|
|
+|
|
|
|
|
|
-|
|
|
+|
|
|
|
|
|
## Known limitations
diff --git a/docs/concepts.md b/docs/concepts.md
index c103745..77f5823 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -67,7 +67,7 @@ Each Connect link is one library. Tools offered depend on the caller's role in t
- **Lengths:** name 200, summary 160, labels 40 each and at most 20. A field without its own limit uses one default per type (text 200, longtext 8,000, markdown 50,000), the same in the tool's argument list and in the save check.
- **Labels** are stored trimmed, lower case and without duplicates, whether they come from an agent, the form or `suggest_labels`. Labels are free-form.
- **Summary** on a plain General library comes from the body; an agent that sends `summary` there is told to put the text in `body`.
-- **Errors** list every problem in one message (with the library's field list), so an agent can fix them in one retry. The tool's argument list shows types, required fields and limits, but the save check enforces them, so a wrong type, a missing field and an unknown field all come back together. Review date versus effective date is compared only when both are real dates.
+- **Errors** list every problem in one message (with the library's field list), so an agent can fix them in one retry. The tool's argument list shows types, required fields and limits, but the save check enforces them, so a wrong type, a missing field and an unknown field all come back together.
- **No match** from `list_items` (by `q` or `entity_key`) is a normal answer that says the item is not in the library, not a tool error.
**How `list_items q=` searches.**
@@ -94,14 +94,12 @@ JSON results (lists, `search_shelf`) carry the same three keys in a `citation` o
Never-published drafts say `citation: none`. Items published before this release start at version 1, with no recorded publisher.
**Freshness.**
-- **Review-by date.** Each published item can have a review-by date, set on every publish (and on Review Unlock saves) to the publish day plus the library's review period.
- - The period defaults to 90 days for Policy and Procedure libraries and is off for other types. Publishers change it under **Review → Freshness settings** (blank means the type default, 0 means off).
- - Policy items use their own **Review date** field instead.
- - A publisher can set or clear the date on the item page.
- - The date lives outside the published copy (`items.review_by`). Changing it is not an edit: it does not go Pending, change the revision, or bump the citation version.
+- **Review-by date.** Each published item can have one review-by date (`items.review_by`), the only review date there is.
+ - Policy and Procedure libraries: every publish (and Review Unlock save) sets it to the publish day plus 90 days.
+ - Other libraries: no automatic date. A publisher sets or clears it on the item page, and later publishes keep it.
+ - The date lives outside the published copy. Changing it is not an edit: it does not go Pending, change the revision, or bump the citation version.
- **Stale answers.** When the review-by date is before today (UTC), the citation adds a `warning` line naming who to ask (the Policy owner field, otherwise the publisher), which agents relay to the user.
-- **Strict mode** (per library, off by default). Items past review are not given as answers. `open_item` and a single-match `list_items` return a refusal telling the user who should review it and where. Lists and `search_shelf` leave those items out and add a `withheld` note.
-- **Review queue.** The library's **Review** tab lists published items due within 14 days or overdue. **Still accurate** re-confirms an item: review-by becomes today plus the period (90 days if the library has none), and who confirmed it and when are recorded and audit-logged (`ui.still_accurate`). Content and version stay the same.
+- **Review queue.** The library's **Review** tab lists published items due within 14 days or overdue. **Still accurate** re-confirms an item: review-by becomes today plus 90 days, and who confirmed it and when are recorded and audit-logged (`ui.still_accurate`). Content and version stay the same.
**Edit conflicts.** Publisher results that return a full item (`open_item`, a single-match `list_items`, `create_item`, `update_item`) include `revision`, an opaque token. Pass it back as `expected_revision` on `update_item`: if anyone saved the item since, nothing is saved and the tool returns a conflict with `current_revision`, so the agent can re-read and reapply. Without `expected_revision` the last save wins, as before. The web edit form does the same check and shows the latest copy when it fails.
diff --git a/migrations/025_single_review_date.sql b/migrations/025_single_review_date.sql
new file mode 100644
index 0000000..66130c2
--- /dev/null
+++ b/migrations/025_single_review_date.sql
@@ -0,0 +1,93 @@
+-- One review date. Policy items used to state their own review_date field,
+-- which was copied into items.review_by on publish; "Still accurate" then
+-- moved only review_by, so the two could disagree. items.review_by is now the
+-- only review date. The per-library review period and strict mode are gone:
+-- Policy and Procedure libraries get publish day + 90 days, other libraries
+-- get no automatic date (a publisher can still set one on the item page).
+
+-- 1. Published Policy items with no review_by yet take the review_date of
+-- their published copy. (Earlier publishes already copied it.)
+UPDATE items i
+SET review_by = (((i.published_snapshot::jsonb ->> 'details')::jsonb) ->> 'review_date')::date
+FROM libraries l
+WHERE l.id = i.library_id
+ AND l.template = 'policy'
+ AND i.review_by IS NULL
+ AND i.published_snapshot IS NOT NULL
+ AND pg_input_is_valid(i.published_snapshot, 'jsonb')
+ AND pg_input_is_valid(i.published_snapshot::jsonb ->> 'details', 'jsonb')
+ AND jsonb_typeof((i.published_snapshot::jsonb ->> 'details')::jsonb) = 'object'
+ AND (((i.published_snapshot::jsonb ->> 'details')::jsonb) ->> 'review_date') ~ '^\d{4}-\d{2}-\d{2}$'
+ AND pg_input_is_valid(((i.published_snapshot::jsonb ->> 'details')::jsonb) ->> 'review_date', 'date');
+
+-- 2. Drop review_date from Policy items: the working row and both published
+-- copies. Not a new publish, so the version and review_by triggers pause.
+ALTER TABLE items DISABLE TRIGGER items_published_version;
+ALTER TABLE items DISABLE TRIGGER items_review_by;
+
+UPDATE items i
+SET details = (i.details::jsonb - 'review_date')::text
+FROM libraries l
+WHERE l.id = i.library_id AND l.template = 'policy'
+ AND pg_input_is_valid(i.details, 'jsonb')
+ AND jsonb_typeof(i.details::jsonb) = 'object'
+ AND i.details::jsonb -> 'review_date' IS NOT NULL;
+
+UPDATE items i
+SET published_snapshot = jsonb_set(
+ i.published_snapshot::jsonb, '{details}',
+ to_jsonb((((i.published_snapshot::jsonb ->> 'details')::jsonb) - 'review_date')::text)
+)::text
+FROM libraries l
+WHERE l.id = i.library_id AND l.template = 'policy'
+ AND pg_input_is_valid(i.published_snapshot, 'jsonb')
+ AND pg_input_is_valid(i.published_snapshot::jsonb ->> 'details', 'jsonb')
+ AND jsonb_typeof((i.published_snapshot::jsonb ->> 'details')::jsonb) = 'object'
+ AND ((i.published_snapshot::jsonb ->> 'details')::jsonb) -> 'review_date' IS NOT NULL;
+
+UPDATE items i
+SET previous_published_snapshot = jsonb_set(
+ i.previous_published_snapshot::jsonb, '{details}',
+ to_jsonb((((i.previous_published_snapshot::jsonb ->> 'details')::jsonb) - 'review_date')::text)
+)::text
+FROM libraries l
+WHERE l.id = i.library_id AND l.template = 'policy'
+ AND pg_input_is_valid(i.previous_published_snapshot, 'jsonb')
+ AND pg_input_is_valid(i.previous_published_snapshot::jsonb ->> 'details', 'jsonb')
+ AND jsonb_typeof((i.previous_published_snapshot::jsonb ->> 'details')::jsonb) = 'object'
+ AND ((i.previous_published_snapshot::jsonb ->> 'details')::jsonb) -> 'review_date' IS NOT NULL;
+
+ALTER TABLE items ENABLE TRIGGER items_published_version;
+ALTER TABLE items ENABLE TRIGGER items_review_by;
+
+-- 3. Policy library schemas lose the Review date field.
+UPDATE libraries
+SET schema = jsonb_set(schema, '{fields}', COALESCE((
+ SELECT jsonb_agg(field ORDER BY pos)
+ FROM jsonb_array_elements(schema -> 'fields') WITH ORDINALITY AS f(field, pos)
+ WHERE field ->> 'key' IS DISTINCT FROM 'review_date'
+), '[]'::jsonb))
+WHERE schema ->> 'starter' = 'policy'
+ AND jsonb_typeof(schema -> 'fields') = 'array'
+ AND EXISTS (SELECT 1 FROM jsonb_array_elements(schema -> 'fields') AS e(field) WHERE field ->> 'key' = 'review_date');
+
+-- 4. On publish: publish day + 90 for Policy and Procedure libraries. Other
+-- libraries keep whatever date a publisher set by hand (or none).
+CREATE OR REPLACE FUNCTION items_set_review_by() RETURNS trigger LANGUAGE plpgsql AS '
+BEGIN
+ IF NEW.published_snapshot IS NOT NULL AND (
+ TG_OP = ''INSERT'' OR NEW.published_snapshot IS DISTINCT FROM OLD.published_snapshot
+ ) AND EXISTS (
+ SELECT 1 FROM libraries WHERE id = NEW.library_id AND template IN (''policy'', ''procedure'')
+ ) THEN
+ NEW.review_by := (to_timestamp(NEW.updated_at / 1000.0) AT TIME ZONE ''UTC'')::date + 90;
+ END IF;
+ RETURN NEW;
+END;
+';
+
+DROP FUNCTION IF EXISTS item_review_by_for(VARCHAR, TEXT, BIGINT);
+DROP FUNCTION IF EXISTS library_review_days(VARCHAR);
+
+ALTER TABLE libraries DROP COLUMN IF EXISTS review_days;
+ALTER TABLE libraries DROP COLUMN IF EXISTS strict_freshness;
diff --git a/node_modules b/node_modules
new file mode 120000
index 0000000..0de9cdd
--- /dev/null
+++ b/node_modules
@@ -0,0 +1 @@
+/workspace/mcpe-src/node_modules
\ No newline at end of file
diff --git a/public/styles.css b/public/styles.css
index e1c0f32..880e8db 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -2355,8 +2355,6 @@ mark.diff-ins {
.inline-form { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0; }
.freshness { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin: 0.5rem 0; }
.freshness .meta { margin: 0; }
-label.checkbox { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; font-weight: 500; }
-label.checkbox input { width: auto; margin: 0; }
/* Settings → Audit log */
.audit-filter { display: flex; gap: 0.75rem; align-items: end; margin: 0 0 1rem; }
diff --git a/scripts/seed-demo.ts b/scripts/seed-demo.ts
index 9cefecf..b1e34af 100644
--- a/scripts/seed-demo.ts
+++ b/scripts/seed-demo.ts
@@ -24,7 +24,7 @@ function policy(name: string, labels: string, summary: string, fields: Record${esc(NOTICES[input.notice])}
` : ""; const rows = input.queue .map( @@ -47,8 +44,6 @@ export function reviewQueuePage(input: { `, ) .join(""); - const defaultLabel = - lib.template === "policy" || lib.template === "procedure" ? `${DEFAULT_REVIEW_DAYS} days` : "off"; return layout({ title: `Review · ${lib.name}`, csrf: input.csrf, @@ -60,23 +55,8 @@ export function reviewQueuePage(input: {Published items due for review within ${REVIEW_QUEUE_DAYS} days, or overdue. Still accurate re-confirms an item and moves its review-by date ${days || DEFAULT_REVIEW_DAYS} days from today without changing its content or version.
+Published items due for review within ${REVIEW_QUEUE_DAYS} days, or overdue. Still accurate re-confirms an item and moves its review-by date ${REVIEW_DAYS} days from today without changing its content or version. ${lib.template === "policy" || lib.template === "procedure" ? `Each publish sets it to ${REVIEW_DAYS} days out.` : "Set a date on an item page to track it here."}
${rows ? `Nothing is due for review in the next ${REVIEW_QUEUE_DAYS} days.
`} -