Scope tag completions to the version
The list came from every version of the source, joined through pf.version, so adjusting this year's plan offered last year's initiatives and counted uses earned somewhere else. A tag names an initiative inside a scenario. A new version now completes from nothing until it has a tag of its own, which is what it actually has: cloning a definition copies no adjustments. The bucket datalist on the Baseline page was already per version, so the two agree now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe456d4e90
commit
05631817ae
21
CLAUDE.md
21
CLAUDE.md
@ -27,7 +27,7 @@ routes/
|
||||
auth.js POST /api/login, /api/logout, GET /api/me; login throttle
|
||||
tables.js GET /api/tables, /api/tables/:schema/:tname/preview
|
||||
sources.js Source registration, col_meta, SQL generation
|
||||
versions.js Version CRUD, baseline/reference load, data stream
|
||||
versions.js Version CRUD, baseline/reference load, data stream, tag completions
|
||||
operations.js scale, recode, clone, undo — the core forecast ops
|
||||
log.js GET /api/versions/:id/log, DELETE /api/log/:logid
|
||||
layouts.js Named pivot layouts — list per version, create, patch, delete
|
||||
@ -323,6 +323,25 @@ together. `columns` selects which *measures* appear, not individual split
|
||||
combinations.
|
||||
|
||||
|
||||
## Tag completions are per version
|
||||
|
||||
`GET /versions/:id/tags` — the tags already used on **this** version, with their
|
||||
use counts, for completing the next adjustment. `Forecast.jsx` refetches on
|
||||
`versionId` and after every tagged write.
|
||||
|
||||
It used to be `GET /sources/:id/tags`, joined through `pf.version` and grouped
|
||||
across every version of the source. A tag names an initiative *inside* a
|
||||
scenario, so that put last year's work in front of this year's and reported a
|
||||
`uses` count earned on a version the tag did not belong to. The cost of the
|
||||
narrower scope is that a fresh version completes from nothing until it has a tag
|
||||
of its own — which is the honest state of it, since cloning a definition copies
|
||||
no adjustments.
|
||||
|
||||
The `pf_bucket` datalist on the Baseline page was already per version
|
||||
(`bucketsInUse`), so the two now agree.
|
||||
|
||||
---
|
||||
|
||||
## Renaming a version
|
||||
|
||||
Safe at any time, including on a closed one. Nothing derives from the name: the
|
||||
|
||||
@ -356,18 +356,29 @@ ${colDefs},
|
||||
// Tags already used on this source, newest first — feeds the tag autocomplete.
|
||||
// Scoped to the source rather than the version so an initiative name carries
|
||||
// across versions, which is the point of naming it.
|
||||
router.get('/sources/:id/tags', async (req, res) => {
|
||||
// Tags already used on one version, for completing the next adjustment.
|
||||
//
|
||||
// Scoped to the version, not the source. A tag names an initiative inside a
|
||||
// scenario -- what this plan is doing -- so offering every tag anyone has
|
||||
// ever used on the source put last year's work in the way of this year's,
|
||||
// and the `uses` count read as activity on a version it did not belong to.
|
||||
// The cost is that a fresh version completes from nothing until it has a
|
||||
// tag of its own, which is the honest state of it: cloning a definition
|
||||
// copies no adjustments, so it has no initiatives yet.
|
||||
router.get('/versions/:id/tags', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT l.tag,
|
||||
count(*)::int AS uses,
|
||||
max(l.stamp) AS last_used
|
||||
FROM pf.log l
|
||||
JOIN pf.version v ON v.id = l.version_id
|
||||
WHERE v.source_id = $1 AND l.tag IS NOT NULL AND l.tag <> ''
|
||||
WHERE TRUE
|
||||
AND l.version_id = $1
|
||||
AND l.tag IS NOT NULL
|
||||
AND l.tag <> ''
|
||||
GROUP BY l.tag
|
||||
ORDER BY max(l.stamp) DESC
|
||||
`, [req.params.id]);
|
||||
`, [parseInt(req.params.id)]);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -664,10 +664,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
} catch { setLogMeta({}) }
|
||||
}
|
||||
|
||||
async function refreshTags(sid) {
|
||||
if (!sid) { setKnownTags([]); return }
|
||||
async function refreshTags(vid) {
|
||||
if (!vid) { setKnownTags([]); return }
|
||||
try {
|
||||
const rows = await fetch(`/api/sources/${sid}/tags`).then(r => r.json())
|
||||
const rows = await fetch(`/api/versions/${vid}/tags`).then(r => r.json())
|
||||
setKnownTags(Array.isArray(rows) ? rows : [])
|
||||
} catch { setKnownTags([]) }
|
||||
}
|
||||
@ -705,7 +705,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
useEffect(() => { refreshLogMeta(versionId) }, [versionId])
|
||||
// Relabelling on the Baseline page needs a reload to show here: the label is
|
||||
// part of the aggregated row, so the pivot cannot re-derive it in place.
|
||||
useEffect(() => { refreshTags(sourceId) }, [sourceId])
|
||||
useEffect(() => { refreshTags(versionId) }, [versionId])
|
||||
|
||||
// Stream an Arrow IPC endpoint into one buffer, reporting download progress.
|
||||
// Both /data and /agg speak the same protocol — a single record batch plus an
|
||||
@ -1621,7 +1621,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
// let the status bar re-read the forecast table's row count
|
||||
window.dispatchEvent(new CustomEvent('pf-data-changed'))
|
||||
refreshLogMeta(versionId)
|
||||
if (opTag.trim()) refreshTags(sourceId)
|
||||
if (opTag.trim()) refreshTags(versionId)
|
||||
if (op === 'scale') { setScaleInputs({}); setScaleNote(''); fetchCurrentTotals(slices) }
|
||||
if (op === 'recode') { setRecodeNote('') }
|
||||
if (op === 'clone') { setCloneNote(''); setCloneScale('1') }
|
||||
@ -1849,7 +1849,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
if (field === 'tag') {
|
||||
// the ledger names its lines from this, and the tag list gains a new entry
|
||||
refreshLogMeta(versionId)
|
||||
refreshTags(sourceId)
|
||||
refreshTags(versionId)
|
||||
}
|
||||
} catch (err) {
|
||||
flash(err.message, 'error')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user