first push
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
type: decision
|
||||
date: 2026-05-19
|
||||
project: cpas
|
||||
tags:
|
||||
- cpas
|
||||
- snapshot
|
||||
- backdating
|
||||
- audit
|
||||
- mpm
|
||||
---
|
||||
|
||||
# [[CPAS]] — back-dated insert refreshes downstream snapshots
|
||||
|
||||
## Context
|
||||
|
||||
On `main` branch (post `ba2b631 documentation`, pre any roll-off model overhaul), the CPAS app stores `prior_active_points` as an immutable snapshot at violation insert time. The PDF endpoint reads this snapshot to render "Prior Active Points → New Total → Tier".
|
||||
|
||||
When a violation is logged with a back-dated `incident_date` that precedes existing violations, the **new** violation's own snapshot is computed correctly, but **existing later-dated violations' snapshots are stale** — they were frozen before the back-dated event existed and so omit it from their 90-day prior window. Re-downloading their PDFs shows the pre-backdate prior/total/tier.
|
||||
|
||||
## Decision
|
||||
|
||||
Refresh downstream snapshots on back-dated insert. Treat back-dating as a *timeline rewrite* and exempt it from the otherwise strict snapshot-immutability rule. Negate, restore, amend, and hard delete are NOT timeline rewrites and continue to leave snapshots alone.
|
||||
|
||||
## Implementation (in `server.js`)
|
||||
|
||||
- New helper `recomputeSnapshotsAfter(employeeId, incidentDate)` finds all violations for the employee with `incident_date > new_date AND incident_date <= new_date + 90 days`, recomputes each via the existing `getPriorActivePoints()` formula, and only writes UPDATEs where the value changed.
|
||||
- `POST /api/violations` now wraps the insert + recompute in a single `db.transaction()` so a partial failure can't leave the system with a new violation and stale sibling snapshots.
|
||||
- When refresh affects ≥1 row, a `violation_snapshots_recomputed` audit entry is written with `reason: 'backdated_insert'`, `trigger_incident_date`, and an `affected[]` array of `{id, incident_date, old, new}` for full traceability.
|
||||
- Forward-dated inserts (the common case) do zero UPDATEs — affected query returns rows but the equality check skips them.
|
||||
|
||||
## Docs
|
||||
|
||||
- `AGENTS.md` "Prior-Points Snapshot" section updated with the back-dating exception
|
||||
- `AGENTS.md` "What NOT to Do" updated — the "do not modify prior_active_points" line now carves out the back-dating path explicitly
|
||||
|
||||
## Verification
|
||||
|
||||
- `node --check server.js` passes
|
||||
- Not runtime-tested in this session (better-sqlite3 won't compile on local Node v24 — see project file). Verification path: docker build, log V1 with a recent date, log V2 with an earlier back-dated date, re-download V1's PDF, confirm "Prior Active Points" now includes V2.
|
||||
|
||||
## Relationship to roll-off model overhaul
|
||||
|
||||
The existing `projects/cpas.md` "Roll-off model fix (2026-05-27)" section describes a *different* implementation of `recomputeSnapshotsAfter` that widens the window to "all later violations" under a clean-cycle roll-off rule. That work is not on `main` as of today. If/when the roll-off overhaul lands on `main`, the helper's window scope will need to be widened to match (current scope is the legacy 90-day rolling window).
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: decision
|
||||
status: shipped
|
||||
tags:
|
||||
- cpas
|
||||
- scoring
|
||||
- roll-off
|
||||
- mpm
|
||||
updated: 2026-05-27
|
||||
---
|
||||
|
||||
# [[CPAS]] roll-off: clean-cycle model, JS as source of truth
|
||||
|
||||
## Context
|
||||
|
||||
System was implementing a literal per-violation rolling 90-day window. Jason reported that the handbook actually says "5 points roll off after 90 consecutive days of no violations" — i.e. any new violation should reset the countdown for everyone, not just for itself.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the clean-cycle model:
|
||||
|
||||
1. Roll-off amount per cycle: **5 points, oldest first**
|
||||
2. Repeat cadence: **another full 90 clean days required** for each successive 5-point roll-off
|
||||
3. Reset trigger: **any new non-negated violation** (negated violations don't reset)
|
||||
|
||||
Implement all standing math in JS (`lib/rolloff.js` `computeStanding()`). Drop the `active_cpas_scores` SQL view — oldest-first partial allocation isn't cleanly SQL-expressible, and having both a SQL and JS implementation invites divergence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Keeping the SQL view with the aggregate formula `total - 5*cycles_since_last_violation` and computing per-violation breakdown only in JS. Rejected: two implementations of the same rule = drift risk; dashboard scale at MPM is small enough that per-request JS computation is fine.
|
||||
- "All points roll off at once after 90 clean days" or "oldest entire violation rolls off." Rejected by Jason in clarifying questions.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `/api/employees/:id/expiration` response shape changed (was per-violation list, now `{ schedule: [...] }`). Frontend `ExpirationTimeline.jsx` updated to match — any third-party consumer of that endpoint would break.
|
||||
- Existing `prior_active_points` snapshots on pre-fix violation rows are stale; existing "Backfill Snapshots" admin button recomputes them under the new model on demand.
|
||||
- `recomputeSnapshotsAfter` no longer caps the affected window at +90 days — backdated inserts now scan all later violations, since under the new model an earlier total shift propagates indefinitely.
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
---
|
||||
type: decision
|
||||
date: 2026-05-28
|
||||
status: open
|
||||
tags: [obsidian, rest-api, bug-report, upstream]
|
||||
related: [[obsidian-memory-plugin]]
|
||||
---
|
||||
|
||||
# 2026-05-28 — File upstream issue for Obsidian Local REST API heading resolver bug
|
||||
|
||||
## Decision
|
||||
|
||||
Draft and file a bug report at [coddingtonbear/obsidian-local-rest-api](https://github.com/coddingtonbear/obsidian-local-rest-api) for the heading-resolver failure that affects both `Target-Type: heading` PATCH and `GET /heading/...` reads.
|
||||
|
||||
## Why
|
||||
|
||||
- Bug persists across a major plugin version bump (3.6 → 4.1.1), so it's not a regression in a transient build — it's a real defect that's been sitting in the resolver for at least one release.
|
||||
- The bug silently degrades the obsidian-memory skill: every documented PATCH-heading example in the skill's `references/api-reference.md` fails, forcing fallbacks to POST/PUT/frontmatter-PATCH. We have working workarounds, but they preclude surgical section edits that the skill instructs Claude to do.
|
||||
- Reproducer is small (4 curl calls) and deterministic — high signal-to-noise for a maintainer.
|
||||
- No alternative — only the upstream maintainer can fix it. Local workarounds will live in [[obsidian-memory-plugin]] indefinitely until this lands.
|
||||
|
||||
## How to apply
|
||||
|
||||
- The full draft (title, repro, variations tried, ruled-out causes, suspected cause) is at `D:\REMOTE CODING\unifi-access-dashboard\obsidian-rest-api-issue-draft.md` on the dashboard machine.
|
||||
- Before filing: fill in two placeholders — Obsidian app version and vault filesystem.
|
||||
- After filing: come back to this decision and update `status:` from `open` → `filed` with the issue URL; if/when fixed, mark `resolved` and remove the workaround section from [[obsidian-memory-plugin]].
|
||||
|
||||
## Context that won't be in the issue body
|
||||
|
||||
- All testing was done against the vault at `https://10.2.0.2:27124` from Claude Code sessions over the LAN.
|
||||
- The skill's documented PATCH-heading example (in `references/api-reference.md`) references a key that has a typo (`8c` instead of `e7` in one digit) — unrelated to this bug but worth noting if anyone tries to copy-paste from there.
|
||||
- `Target-Type: block` also fails with `invalid-target`. Probably the same resolver but not verified — left out of the issue body to keep the report tight; can add as a follow-up comment if relevant.
|
||||
|
||||
## Open
|
||||
|
||||
- [ ] Fill in placeholders in the draft and post the issue
|
||||
- [ ] Paste the issue URL back here under "How to apply"
|
||||
- [ ] When fixed upstream: drop the workaround section from [[obsidian-memory-plugin]] and switch the skill back to PATCH-heading where it makes sense
|
||||
|
||||
## Draft body (copy-paste ready)
|
||||
|
||||
See `obsidian-rest-api-issue-draft.md` on the dashboard machine, or mirrored below:
|
||||
|
||||
---
|
||||
|
||||
### Heading resolver broken: `Target-Type: heading` PATCH and `GET /heading/...` both fail on any valid heading (4.1.1)
|
||||
|
||||
**Summary**
|
||||
|
||||
In Obsidian Local REST API **4.1.1**, two endpoints that both rely on heading resolution always fail, even on a freshly written file with trivially-valid markdown headings:
|
||||
|
||||
- `PATCH /vault/<path>` with `Target-Type: heading` → HTTP 400 `invalid-target`
|
||||
- `GET /vault/<path>/heading/<heading-text>` → HTTP 404 `Not Found`
|
||||
|
||||
`Target-Type: frontmatter` PATCH on the same file works fine, so the PATCH route itself is healthy. The bug is isolated to the heading-resolution code path shared by these two endpoints.
|
||||
|
||||
Also reproduced in **3.6** before upgrading; the 3.6 → 4.1.1 upgrade did not change the behavior.
|
||||
|
||||
**Minimal reproducer**
|
||||
|
||||
```bash
|
||||
HOST="https://YOUR-HOST:27124"
|
||||
KEY="YOUR_API_KEY"
|
||||
|
||||
# 1. PUT a fresh, simple file (HTTP 204 expected)
|
||||
cat > /tmp/repro.md <<'EOF'
|
||||
---
|
||||
type: scratch
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
## Section A
|
||||
|
||||
Content under A.
|
||||
|
||||
## Section B
|
||||
|
||||
Content under B.
|
||||
EOF
|
||||
|
||||
curl -sk -w "PUT: %{http_code}\n" -X PUT \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H "Content-Type: text/markdown" \
|
||||
--data-binary @/tmp/repro.md \
|
||||
"$HOST/vault/scratch/repro.md"
|
||||
|
||||
# 2. PATCH append under heading "Section A" — expected 200, actual 400 invalid-target
|
||||
echo "appended" > /tmp/p.md
|
||||
curl -sk -w "\nPATCH heading: %{http_code}\n" -X PATCH \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H "Operation: append" \
|
||||
-H "Target-Type: heading" \
|
||||
-H "Target: Section A" \
|
||||
-H "Content-Type: text/markdown" \
|
||||
--data-binary @/tmp/p.md \
|
||||
"$HOST/vault/scratch/repro.md"
|
||||
|
||||
# 3. GET the heading's content via URL path — expected 200, actual 404
|
||||
curl -sk -w "\nGET heading: %{http_code}\n" \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
"$HOST/vault/scratch/repro.md/heading/Section%20A"
|
||||
|
||||
# 4. CONTROL: PATCH frontmatter — works (HTTP 200)
|
||||
curl -sk -w "\nPATCH frontmatter: %{http_code}\n" -X PATCH \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H "Operation: replace" \
|
||||
-H "Target-Type: frontmatter" \
|
||||
-H "Target: type" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '"updated"' \
|
||||
"$HOST/vault/scratch/repro.md"
|
||||
```
|
||||
|
||||
**Actual output**
|
||||
|
||||
```
|
||||
PUT: 204
|
||||
{"message":"...invalid-target","errorCode":40080} PATCH heading: 400
|
||||
{"message":"Not Found","errorCode":40400} GET heading: 404
|
||||
PATCH frontmatter: 200
|
||||
```
|
||||
|
||||
**Variations tried (all fail identically with `invalid-target` / 404)**
|
||||
|
||||
- Single-word heading (`Target: Section`) and multi-word (`Target: Section A`)
|
||||
- URL-encoded header value (`Target: Section%20A`)
|
||||
- `Operation: append`, `prepend`, and `replace`
|
||||
- Both freshly-PUT files (LF endings) and files originally written by Obsidian
|
||||
- `Target-Type: block` also returns `invalid-target`
|
||||
|
||||
**What I ruled out**
|
||||
|
||||
- CRLF / BOM in source file — fresh file was written by `curl --data-binary` with LF-only content
|
||||
- Spaces in heading text — single-word headings fail the same way
|
||||
- URL encoding of `Target:` header value — raw and percent-encoded both fail
|
||||
- PATCH route generally broken — `Target-Type: frontmatter` succeeds on the same file
|
||||
- Version regression in 4.1.1 — same behavior in 3.6 before upgrading
|
||||
|
||||
**Environment**
|
||||
|
||||
- Obsidian Local REST API: 4.1.1 (also reproduced in 3.6)
|
||||
- Obsidian: `<fill in>`
|
||||
- OS hosting Obsidian: `<fill in>`
|
||||
- Client: curl over HTTPS to :27124 with `-k` (self-signed cert)
|
||||
- Vault filesystem: `<fill in>`
|
||||
|
||||
**Suspected cause**
|
||||
|
||||
Since `GET /vault/<path>/heading/<name>` and `Target-Type: heading` PATCH fail in lockstep (and frontmatter resolution works fine on the same request path), the most likely location is whichever helper enumerates headings within a parsed file's AST — perhaps it's getting an empty list, or matching on the wrong field.
|
||||
|
||||
Happy to bisect or add logging if you can point me at the relevant file.
|
||||
Reference in New Issue
Block a user