mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:51:59 +00:00
目录修改提案落在已驳回的项上时,改前写出被驳回的原值:plan_patch 交回的 before 带上驳回的值、before_status 写 rejected(只改给人看的信息,算改后、算状态照旧按没有这一项,保存时照旧换成人工确认的新值);助手流的提案卡片、表助手的提案卡片和待保存的修改写「已驳回:<原值>」,和导入预览「目标里这一项已经驳回」一致;check-catalog-loop、check-stream 补对应检查
This commit is contained in:
@@ -878,7 +878,8 @@ class PatchChange:
|
||||
"""提案里的一项,核对过的。
|
||||
|
||||
- path:规范化后的路径(新增关系写成 relations.<编号>);
|
||||
- before / before_status:当前目录里这一项的值和状态(被驳回的和没有的都算没有,为 None);
|
||||
- before / before_status:当前目录里这一项的值和状态,没有这一项为 None。被驳回的照样给出原值、状态写 rejected
|
||||
(同导入预览「目标里这一项已经驳回」):只是给人看的,算改后、算 state 都按没有这一项,保存时换成人工确认的新值;
|
||||
- after:保存后的值(码值是补充后的完整对照);value:要交回保存的原样取值(码值只有补充的那几个,
|
||||
409 之后在最新的目录上重新补);
|
||||
- state:change 值有变化;confirm 值相同、但还不是已确认(保存即确认);same 已经是这个值且已确认。
|
||||
@@ -1014,6 +1015,8 @@ def plan_patch(notes: Mapping[str, Any] | None, changes: Any, *, table: str,
|
||||
没有这个码时加进去,不会把已有的含义抹掉(_merge_codes)。
|
||||
- 关联关系不抹掉测量结果:覆盖率沿用目录里的,基数没写时沿用或推算(_relation_after)。两端、基数、覆盖率都和
|
||||
目录里一样时只是确认,来源不变。
|
||||
- 被驳回的项算没有:码值不在驳回的码值上补,和驳回的值一样也是 change(保存时换成人工确认的新值)。改前照样写出
|
||||
驳回的原值、before_status 为 rejected,不然界面上只看到「未填写」,看不出原来有一个被驳回的值。
|
||||
- 看起来像计算公式的文字不收(_FORMULA):公式进口径卡。
|
||||
- 同一个路径出现两次只认第一次。
|
||||
"""
|
||||
@@ -1094,8 +1097,10 @@ def plan_patch(notes: Mapping[str, Any] | None, changes: Any, *, table: str,
|
||||
state = "same" if live.get("status") == "confirmed" else "confirm"
|
||||
else:
|
||||
state = "change"
|
||||
out.append(PatchChange(path=norm, before=copy.deepcopy(before),
|
||||
before_status=live.get("status") if live is not None else None,
|
||||
# 改前给人看:被驳回的也写出原值(上面算改后、算 state 照旧按没有这一项)
|
||||
shown = live if live is not None else prev
|
||||
out.append(PatchChange(path=norm, before=copy.deepcopy(_value_of(key, shown)) if shown is not None else None,
|
||||
before_status=shown.get("status") if shown is not None else None,
|
||||
after=after, value=value, reason=reason, state=state, note=note))
|
||||
return PatchPlan(out, problems)
|
||||
|
||||
|
||||
@@ -204,6 +204,36 @@ async def test_saving_keeps_the_reason_as_the_note(scenic_db):
|
||||
assert catalog.validate_notes(out) == []
|
||||
|
||||
|
||||
async def test_rejected_items_show_their_value_before(scenic_db):
|
||||
"""提案落在已驳回的项上:改前带上被驳回的原值、状态写已驳回(同导入预览「目标里这一项已经驳回」)。只改预览给出的
|
||||
信息,保存的规矩不变:照旧换成人工确认的新值;码值不在驳回的码值上补;和驳回的值一样的也不算「值不变」,照样存成
|
||||
人工确认。"""
|
||||
tables = (await _schema(scenic_db))["tables"]
|
||||
rejected = scenic_notes.notes("rejected")["visits"]
|
||||
rel_value = {"columns": ["park_id"], "to_table": "parks", "to_columns": ["id"], "cardinality": "many_to_one"}
|
||||
changes = [
|
||||
{"path": "columns.visitor_count.measure", "value": "stock"},
|
||||
{"path": "columns.status.codes", "value": {"9": "作废"}},
|
||||
{"path": "valid_filter", "value": "status = 1"}, # 和驳回的值一样
|
||||
{"path": f"relations.{_PARKS}", "value": rel_value}, # 两端、基数都和驳回的那条一样
|
||||
]
|
||||
plan = catalog.plan_patch(rejected, changes, table="visits", tables=tables)
|
||||
assert plan.problems == []
|
||||
measure, codes, valid, rel = plan.changes
|
||||
assert (measure.before, measure.before_status, measure.state) == ("flow", "rejected", "change")
|
||||
assert (codes.before, codes.before_status, codes.state) == ({"1": "有效", "0": "作废"}, "rejected", "change")
|
||||
assert codes.after == {"9": "作废"}, "码值不在驳回的码值上补"
|
||||
assert (valid.before, valid.before_status, valid.state) == ("status = 1", "rejected", "change")
|
||||
assert (rel.before, rel.before_status, rel.state) == ({**rel_value, "coverage": None}, "rejected", "change")
|
||||
assert rel.as_dict()["before_status"] == "rejected" and rel.as_dict()["before"]["to_table"] == "parks"
|
||||
out = catalog.apply_patch(rejected, changes, table="visits", tables=tables)
|
||||
saved = [out["columns"]["visitor_count"]["measure"], out["columns"]["status"]["codes"], out["valid_filter"],
|
||||
next(r for r in out["relations"] if r["id"] == _PARKS)]
|
||||
assert all((item["source"], item["status"]) == ("human", "confirmed") for item in saved)
|
||||
codes = out["columns"]["status"]["codes"]
|
||||
assert codes["value"] == {"9": "作废"} and "complete" not in codes
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 协议
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -2610,6 +2610,8 @@ export const CATALOG_PATCH_TEXT = {
|
||||
after: '改后',
|
||||
reason: '理由',
|
||||
empty: '未填写',
|
||||
/** 改前是一个被驳回的值(保存后换成人工确认的新值):同导入预览「目标里这一项已经驳回」 */
|
||||
beforeRejected: (value: string) => `已驳回:${value}`,
|
||||
stateConfirm: '值不变,保存即确认',
|
||||
stateSame: '已是这个值',
|
||||
save: '保存到数据目录',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Notice, Spinner, confirmDialog, isComposing, toast } from '../../compon
|
||||
import { useLeaveGuard } from '../../lib/leave'
|
||||
import { CATALOG_ASSISTANT_TEXT as T, CATALOG_STATUS_LABEL } from '../../lib/terms'
|
||||
import { Markdown } from '../../run/Markdown'
|
||||
import { patchValueText } from '../../run/catalogPatch'
|
||||
import { beforeStatusShown, patchBeforeText, patchValueText } from '../../run/catalogPatch'
|
||||
import {
|
||||
ASSISTANT_MAX_CHARS, ASSISTANT_MAX_SAVE, QUERY_ROWS_SHOWN, applyEvent, applyPreview, assistantChangesOf, blankColumnsOf, blankLeadOf,
|
||||
changeStateOf, changeWhere, dropPending, historyOf, mergeProposal, pendingForAssistant, pendingSubmit, pendingVersion, savedKey,
|
||||
@@ -495,9 +495,10 @@ function ChangeRow({ change: c, state, onToggle }: {
|
||||
</div>
|
||||
<dl className={clsx('mt-1 grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5 leading-relaxed', off && 'opacity-70')}>
|
||||
<dt className="text-faint">{T.before}</dt>
|
||||
<dd className={clsx('break-words', c.before == null ? 'text-faint' : 'text-dim')} data-change-before="">
|
||||
{valueText(c.path, c.before)}
|
||||
{c.before != null && c.before_status && <span className="text-faint">({CATALOG_STATUS_LABEL[c.before_status]})</span>}
|
||||
<dd className={clsx('break-words', c.before == null || c.before_status === 'rejected' ? 'text-faint' : 'text-dim')} data-change-before=""
|
||||
data-change-before-status={c.before_status ?? undefined}>
|
||||
{patchBeforeText(c)}
|
||||
{beforeStatusShown(c) && <span className="text-faint">({CATALOG_STATUS_LABEL[c.before_status!]})</span>}
|
||||
</dd>
|
||||
<dt className="text-faint">{T.after}</dt>
|
||||
<dd className="break-words text-fg" data-change-after="">
|
||||
@@ -585,7 +586,7 @@ function PendingTray({ pending, open, onToggle, save, editing, onDrop, onClear,
|
||||
<div className="min-w-0 flex-1 leading-relaxed">
|
||||
<p className="break-words font-medium text-fg">{where}</p>
|
||||
<p className="break-words text-dim">
|
||||
<span className="text-faint">{T.before} </span>{valueText(p.path, p.before)}
|
||||
<span className="text-faint">{T.before} </span>{patchBeforeText(p)}
|
||||
<span className="text-faint"> → {T.after} </span><span className="text-fg">{valueText(p.path, p.after)}</span>
|
||||
</p>
|
||||
{p.reason && <p className="break-words text-faint"><span>{T.reason}:</span>{p.reason}</p>}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { CatalogImpactList } from '../components/CatalogImpact'
|
||||
import { CATALOG_PATCH_TEXT as T, CATALOG_STATUS_LABEL, CATALOG_UI_TEXT as UT } from '../lib/terms'
|
||||
import type { CatalogPatchChange } from '../types'
|
||||
import {
|
||||
catalogTableHref, codeMeaning, patchSubmit, patchTarget, patchValueText, patchWhere, type CatalogPatch,
|
||||
beforeStatusShown, catalogTableHref, codeMeaning, patchBeforeText, patchSubmit, patchTarget, patchValueText, patchWhere,
|
||||
type CatalogPatch,
|
||||
} from './catalogPatch'
|
||||
|
||||
// ===========================================================================
|
||||
@@ -183,13 +184,15 @@ function ChangeRow({ change: c, dense }: { change: CatalogPatchChange; dense: bo
|
||||
</div>
|
||||
<dl className="mt-1 grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5 text-2xs leading-relaxed">
|
||||
<dt className="text-faint">{T.before}</dt>
|
||||
<dd className={clsx('break-words', c.before == null ? 'text-faint' : 'text-dim')} data-patch-before="">
|
||||
{patchValueText(c.path, c.before)}
|
||||
{c.before != null && c.before_status && <span className="text-faint">({CATALOG_STATUS_LABEL[c.before_status]})</span>}
|
||||
<dd className={clsx('break-words', c.before == null || c.before_status === 'rejected' ? 'text-faint' : 'text-dim')} data-patch-before=""
|
||||
data-patch-before-status={c.before_status ?? undefined}>
|
||||
{patchBeforeText(c)}
|
||||
{beforeStatusShown(c) && <span className="text-faint">({CATALOG_STATUS_LABEL[c.before_status!]})</span>}
|
||||
</dd>
|
||||
<dt className="text-faint">{T.after}</dt>
|
||||
<dd className="break-words text-fg" data-patch-after="">
|
||||
{isCodes ? <CodesAfter before={c.before} after={c.after} /> : patchValueText(c.path, c.after)}
|
||||
{/* 被驳回的码值不算:保存时整项换成新的,改后的码值都是新加的 */}
|
||||
{isCodes ? <CodesAfter before={c.before_status === 'rejected' ? null : c.before} after={c.after} /> : patchValueText(c.path, c.after)}
|
||||
</dd>
|
||||
{c.reason && (
|
||||
<>
|
||||
|
||||
@@ -141,6 +141,20 @@ export function patchValueText(path: string, v: unknown): string {
|
||||
return typeof v === 'string' ? v : JSON.stringify(v)
|
||||
}
|
||||
|
||||
/**
|
||||
* 改前那一格(卡片、表助手的提案和待保存的修改都用它):被驳回的项写「已驳回:原值」——服务端把驳回的原值也交回来
|
||||
* (before_status 为 rejected),保存时照旧换成人工确认的新值;只写「未填写」就看不出原来有一个被驳回的值,和导入预览
|
||||
* 「目标里这一项已经驳回」对不上。别的照 patchValueText 写,没有值写「未填写」
|
||||
*/
|
||||
export function patchBeforeText(change: Pick<CatalogPatchChange, 'path' | 'before' | 'before_status'>): string {
|
||||
const text = patchValueText(change.path, change.before)
|
||||
return change.before_status === 'rejected' && change.before != null ? CATALOG_PATCH_TEXT.beforeRejected(text) : text
|
||||
}
|
||||
|
||||
/** 改前要不要跟一个状态(「(推断)」):有值、不是被驳回的(被驳回的状态已经写在 patchBeforeText 里) */
|
||||
export const beforeStatusShown = (change: Pick<CatalogPatchChange, 'before' | 'before_status'>) =>
|
||||
change.before != null && !!change.before_status && change.before_status !== 'rejected'
|
||||
|
||||
/** 交回保存、预览的那一份:路径、原样取值、理由 */
|
||||
export function patchSubmit(changes: CatalogPatchChange[]) {
|
||||
return changes.map((c) => ({ path: c.path, value: c.value, ...(c.reason ? { reason: c.reason } : {}) }))
|
||||
|
||||
@@ -307,7 +307,7 @@ function applyPatch(notes, changes) {
|
||||
}
|
||||
return assemble(map)
|
||||
}
|
||||
/** 预览:对着当前目录算改前、改后(只读) */
|
||||
/** 预览:对着当前目录算改前、改后(只读)。被驳回的项改前照样写出原值、状态写已驳回,改后和状态按没有这一项算 */
|
||||
function previewOf(t, changes) {
|
||||
const map = slots(t.notes ?? {})
|
||||
return {
|
||||
@@ -316,8 +316,8 @@ function previewOf(t, changes) {
|
||||
const prev = map.get(c.path)
|
||||
const live = prev && prev.status !== 'rejected' ? prev : null
|
||||
const after = c.path.endsWith('.codes') && live ? { ...live.value, ...c.value } : c.value
|
||||
return { path: c.path, before: live?.value ?? null, before_status: live?.status ?? null, after, value: c.value, reason: c.reason ?? '',
|
||||
state: JSON.stringify(live?.value) === JSON.stringify(after) ? (live.status === 'confirmed' ? 'same' : 'confirm') : 'change' }
|
||||
return { path: c.path, before: prev?.value ?? null, before_status: prev?.status ?? null, after, value: c.value, reason: c.reason ?? '',
|
||||
state: live && JSON.stringify(live.value) === JSON.stringify(after) ? (live.status === 'confirmed' ? 'same' : 'confirm') : 'change' }
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -625,6 +625,15 @@ await section('纯函数 · 对话怎么截、待保存的修改怎么累积、
|
||||
dedup: { value: 'y', source: 'llm', status: 'rejected' } }, 4, res.outcomes, res.versions)
|
||||
out.sync = { grain: synced.outcomes.get(rv.itemKey(items[0])), dedup: synced.outcomes.get(rv.itemKey(items[1]))?.status, v: synced.versions.get('t1') }
|
||||
out.pct = [rv.percentOf(120, 286), rv.percentOf(0, 0)]
|
||||
// 提案的改前:被驳回的原值也写出来(同导入预览「目标里这一项已经驳回」)
|
||||
const cp = await import('/src/run/catalogPatch.ts')
|
||||
out.before = [
|
||||
cp.patchBeforeText({ path: 'columns.amount.unit', before: '元', before_status: 'rejected' }),
|
||||
cp.patchBeforeText({ path: 'columns.amount.unit', before: '元', before_status: 'proposed' }),
|
||||
cp.patchBeforeText({ path: 'columns.amount.unit', before: null, before_status: null }),
|
||||
cp.patchBeforeText({ path: 'kind', before: 'fact', before_status: 'rejected' }),
|
||||
cp.patchBeforeText({ path: 'columns.status.codes', before: { 1: '有效' }, before_status: 'rejected' }),
|
||||
]
|
||||
// 审阅队列一项的位置:按 path 说到列(服务端的 field_label 只是字段的叫法)
|
||||
out.where = [
|
||||
rv.reviewWhere({ path: 'columns.status.codes', field_label: '码值', column: 'status', value: { 1: '有效' } }),
|
||||
@@ -687,6 +696,8 @@ await section('纯函数 · 对话怎么截、待保存的修改怎么累积、
|
||||
check('表详情改过之后:回到推断的记成撤销审阅,驳回的记成已驳回,版本换成最新的', r.sync.grain?.action === 'reset' && r.sync.dedup === 'rejected' && r.sync.v === 4,
|
||||
JSON.stringify(r.sync))
|
||||
check('进度的百分比四舍五入,分母为 0 时不写', r.pct[0] === 42 && r.pct[1] === null, JSON.stringify(r.pct))
|
||||
check('提案的改前:被驳回的写「已驳回:原值」(值按界面上的说法),别的照原样,没有值写「未填写」',
|
||||
JSON.stringify(r.before) === JSON.stringify(['已驳回:元', '元', '未填写', '已驳回:明细表', '已驳回:1=有效']), JSON.stringify(r.before))
|
||||
check('队列一项的位置写到列:列 status 的码值、表的有效记录条件、指向 parks 的关联关系;列名里有句点、认不出的路径也说得通',
|
||||
JSON.stringify(r.where) === JSON.stringify(['列 status 的码值', '表的有效记录条件', '指向 parks 的关联关系', '列 a.b 的中文名', '列 x 的怪字段', '怪字段']),
|
||||
JSON.stringify(r.where))
|
||||
@@ -1439,6 +1450,41 @@ await section('表详情 · 人工确认的项的说明', async () => {
|
||||
await close()
|
||||
})
|
||||
|
||||
await section('表助手 · 改前是已驳回的值', async () => {
|
||||
// 单位「元」被驳回过:助手提「人」,改前写「已驳回:元」(不是「未填写」),和导入预览「目标里这一项已经驳回」一致
|
||||
const state = freshState()
|
||||
const visits = state.tables[S1].find((t) => t.name === 'visits')
|
||||
visits.notes.columns.visitor_count.unit = item('元', 'llm', 'rejected')
|
||||
const PROPOSAL_R = {
|
||||
type: 'proposal', source: NAME1, source_id: S1, table: 'visits', table_label: '入园记录', version: 2, changes: [
|
||||
{ path: 'columns.visitor_count.unit', label: '列 visitor_count 的单位', before: '元', before_status: 'rejected', after: '人', value: '人',
|
||||
reason: '使用者说:入园人数按人计', state: 'change' },
|
||||
{ path: 'kind', label: '表的表类型', before: null, before_status: null, after: 'fact', value: 'fact', reason: '使用者说:一行一次入园', state: 'change' },
|
||||
],
|
||||
}
|
||||
const replies = { assistant: [sse([{ type: 'text', delta: '好的,单位按人计。' }, PROPOSAL_R, done()])] }
|
||||
const { page, sent, errors, close } = await open(`/data/catalog/${S1}/visits`, { handlers: loopHandlers(state, replies) })
|
||||
await until(async () => (await page.locator('[data-catalog-detail="visits"][data-version]').count()) > 0)
|
||||
await page.locator('[data-catalog-assistant-open="header"]').click()
|
||||
await until(async () => panel(page).isVisible())
|
||||
await say(page, '入园人数按人计')
|
||||
const unit = panel(page).locator('[data-assistant-change="columns.visitor_count.unit"]')
|
||||
await shot(page, 'catalog-assistant-rejected')
|
||||
check('提案卡片的改前:「已驳回:元」,不写「未填写」', (await textOf(unit.locator('[data-change-before]'))) === '已驳回:元', await textOf(unit))
|
||||
check('……原来没有的照旧写「未填写」', (await textOf(panel(page).locator('[data-assistant-change="kind"] [data-change-before]'))) === '未填写')
|
||||
const tray = panel(page).locator('[data-assistant-pending-item="columns.visitor_count.unit"]')
|
||||
check('待保存的修改里同样写「改前 已驳回:元」', (await textOf(tray)).includes('改前 已驳回:元'), await textOf(tray))
|
||||
await panel(page).locator('[data-assistant-save]').click()
|
||||
check('保存:交回原样取值和依据', await until(async () => count(sent, /\/visits\/patch$/) === 1)
|
||||
&& JSON.stringify(lastSent(sent, /\/visits\/patch$/)?.body?.changes?.[0]) === JSON.stringify({ path: 'columns.visitor_count.unit', value: '人', reason: '使用者说:入园人数按人计' }))
|
||||
check('……存上以后那一格是人工确认的「人」,悬停写保存时的依据', await until(async () => (await version(page)) === 3)
|
||||
&& (await textOf(page.locator('[data-column="visitor_count"] [data-cell="unit"]'))).includes('人')
|
||||
&& ((await page.locator('[data-column="visitor_count"] [data-cell="unit"] [data-cell-note]').getAttribute('title').catch(() => '')) ?? '').includes('依据:使用者说:入园人数按人计'))
|
||||
await cleanCopy(page, '改前是已驳回的值')
|
||||
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
|
||||
await close()
|
||||
})
|
||||
|
||||
await section('防错事实的提议 · 表详情里的说明', async () => {
|
||||
const state = freshState()
|
||||
const { page, errors, close } = await open(`/data/catalog/${S1}/visits`, { handlers: loopHandlers(state) })
|
||||
|
||||
@@ -822,12 +822,14 @@ await section('建议更新数据目录:卡片的保存、忽略和 409(cata
|
||||
const body = req.postDataJSON()
|
||||
sent.push({ url: req.url(), body, actor: req.headers()['x-actor'] })
|
||||
if (req.url().endsWith('/patch/preview')) {
|
||||
// 重新载入:别人刚把码值补了一个 8,改前改后按最新的算
|
||||
// 重新载入:别人刚把码值补了一个 8、把度量类型驳回了,改前改后按最新的算(驳回的项改前照样写出原值)
|
||||
return route.fulfill({ json: { table_name: 'visits', version: 4, problems: [], changes: [
|
||||
{ path: 'columns.status.codes', before: { 1: '有效', 0: '作废', 8: '退票' }, before_status: 'confirmed',
|
||||
after: { 1: '有效', 0: '作废', 8: '退票', 9: '作废' }, value: { 9: '作废' }, reason: '用户说明 status=9 表示作废', state: 'change' },
|
||||
{ path: 'valid_filter', before: 'status = 1', before_status: 'proposed', after: 'status = 1 AND status <> 9',
|
||||
value: 'status = 1 AND status <> 9', reason: '统计时要排除作废记录', state: 'change' },
|
||||
{ path: 'columns.visitor_count.measure', before: 'flow', before_status: 'rejected', after: 'flow', value: 'flow',
|
||||
reason: '', state: 'change' },
|
||||
] } })
|
||||
}
|
||||
if (conflictOnce) {
|
||||
@@ -871,6 +873,8 @@ await section('建议更新数据目录:卡片的保存、忽略和 409(cata
|
||||
check('重新载入交回的是助手原来的提案', sent[1]?.url.endsWith('/patch/preview')
|
||||
&& JSON.stringify(sent[1]?.body?.changes?.[0]?.value) === '{"9":"作废"}', JSON.stringify(sent[1]))
|
||||
check('重新载入后改前按最新的目录写', (await codes.locator('[data-patch-before]').innerText()).includes('8=退票'))
|
||||
check('……刚被驳回的项:改前写「已驳回:可累加」,不写「未填写」', (await card.locator('[data-patch-change="columns.visitor_count.measure"] [data-patch-before]')
|
||||
.innerText()) === '已驳回:可累加', await card.locator('[data-patch-change="columns.visitor_count.measure"]').innerText())
|
||||
await card.locator('[data-patch-save]').click()
|
||||
await card.locator('[data-patch-done]').waitFor({ timeout: 3000 })
|
||||
check('再保存带新版本', sent[2]?.body?.if_version === 4, JSON.stringify(sent[2]?.body))
|
||||
|
||||
Reference in New Issue
Block a user