Implement CE-WP-0012 attribute types; freeze accepted catalog defaults.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Demo schema exercises amount and one-of; capture-state validates options;
proposal marked accepted; workplan finished.
This commit is contained in:
tegwick 2026-07-30 21:05:44 +02:00
parent d4327b6be6
commit 36d8d3a73f
9 changed files with 117 additions and 56 deletions

View file

@ -18,6 +18,7 @@
| workplan | CE-WP-0008 | done | — | workplans/CE-WP-0008-capture-content-editing.md |
| workplan | CE-WP-0009 | done | — | workplans/CE-WP-0009-engine-workspace-wireup.md |
| workplan | CE-WP-0010 | finished | — | workplans/CE-WP-0010-annotate-attributes-ux.md |
| workplan | CE-WP-0011 | finished | — | workplans/CE-WP-0011-attributes-guide-nm.md |
| task | CE-WP-0001-T01 | done | — | workplans/CE-WP-0001-foundations.md |
| task | CE-WP-0001-T02 | done | — | workplans/CE-WP-0001-foundations.md |
| task | CE-WP-0001-T03 | done | — | workplans/CE-WP-0001-foundations.md |
@ -89,3 +90,7 @@
| task | CE-WP-0010-T05 | done | — | workplans/CE-WP-0010-annotate-attributes-ux.md |
| task | CE-WP-0010-T06 | done | — | workplans/CE-WP-0010-annotate-attributes-ux.md |
| task | CE-WP-0010-T07 | done | — | workplans/CE-WP-0010-annotate-attributes-ux.md |
| task | CE-WP-0011-T01 | done | — | workplans/CE-WP-0011-attributes-guide-nm.md |
| task | CE-WP-0011-T02 | done | — | workplans/CE-WP-0011-attributes-guide-nm.md |
| task | CE-WP-0011-T03 | done | — | workplans/CE-WP-0011-attributes-guide-nm.md |
| task | CE-WP-0011-T04 | done | — | workplans/CE-WP-0011-attributes-guide-nm.md |

View file

@ -33,7 +33,7 @@ publish tasks wait on ADR-0002 resolution.
|----------|-------|--------|
| `CE-WP-0010` | Annotate & Attributes UX — labels, filters, layout, evidence connectors | finished |
| `CE-WP-0011` | Attributes guide n:m lines, title migration, Add attribute placement | finished |
| `CE-WP-0012` | Attribute value types — time, datetime, amount, one-of, some-of | proposed |
| `CE-WP-0012` | Attribute value types — time, datetime, amount, one-of, some-of | finished |
User-facing polish after manual document review: rename Review→Annotate, keep
Evidence column in Capture with Attributes on the right, list filters, and

View file

@ -93,6 +93,10 @@ export function FormsApp({
id,
type: patch.type,
label: patch.label.length > 0 ? patch.label : `New attribute ${n}`,
...(patch.options ? { options: patch.options } : {}),
...(patch.defaultCurrency
? { defaultCurrency: patch.defaultCurrency }
: {}),
};
return { ...prev, fields: [...prev.fields, field] };
});
@ -105,15 +109,19 @@ export function FormsApp({
(fieldId: string, patch: FieldDefinitionPatch) => {
setSchema((prev) => ({
...prev,
fields: prev.fields.map((f) =>
f.id === fieldId
? {
...f,
type: patch.type,
label: patch.label.length > 0 ? patch.label : f.label,
}
: f,
),
fields: prev.fields.map((f) => {
if (f.id !== fieldId) return f;
const next: FormFieldSchema = {
id: f.id,
type: patch.type,
label: patch.label.length > 0 ? patch.label : f.label,
};
if (patch.options) return { ...next, options: patch.options };
if (patch.defaultCurrency) {
return { ...next, defaultCurrency: patch.defaultCurrency };
}
return next;
}),
}));
setEditingFieldId(null);
},

View file

@ -1,15 +1,19 @@
/**
* Per-session Capture mode persistence (form schema, field values, links).
*
* Engine snapshots intentionally omit binder/app UI state. This module
* stores capture data beside the engine snapshot under a per-session
* localStorage key.
* Amount wire form (CE-WP-0012): JSON string `{"value":"…","currency":"EUR"}`.
* some-of wire form: JSON array of option ids.
*/
import type { EvidenceLink } from "@shared/evidence-link";
import type { SessionId } from "@shared/ids";
import type { FormSchema } from "@binder/FormRenderer";
import {
isAttributeValueType,
normalizeFormSchema,
type FormFieldSchema,
type FormSchema,
} from "@binder/FormRenderer";
import { DEMO_SCHEMA } from "./demo-schema";
@ -35,20 +39,37 @@ export function defaultCaptureState(): CaptureStateSnapshot {
};
}
function isAttributeOption(value: unknown): boolean {
if (typeof value !== "object" || value === null) return false;
const o = value as Record<string, unknown>;
return typeof o.id === "string" && typeof o.label === "string";
}
function isFormField(value: unknown): value is FormFieldSchema {
if (typeof value !== "object" || value === null) return false;
const field = value as Record<string, unknown>;
if (typeof field.id !== "string" || typeof field.label !== "string") return false;
if (!isAttributeValueType(field.type)) return false;
if (field.options !== undefined) {
if (!Array.isArray(field.options) || !field.options.every(isAttributeOption)) {
return false;
}
}
if (
field.defaultCurrency !== undefined &&
typeof field.defaultCurrency !== "string"
) {
return false;
}
return true;
}
function isFormSchema(value: unknown): value is FormSchema {
if (typeof value !== "object" || value === null) return false;
const o = value as Record<string, unknown>;
if (typeof o.id !== "string" || typeof o.title !== "string") return false;
if (!Array.isArray(o.fields)) return false;
return o.fields.every((f) => {
if (typeof f !== "object" || f === null) return false;
const field = f as Record<string, unknown>;
return (
typeof field.id === "string" &&
typeof field.label === "string" &&
(field.type === "text" || field.type === "textarea" || field.type === "date")
);
});
return o.fields.every(isFormField);
}
/** Legacy Capture titles from pre-CE-WP-0010 sessions. */
@ -59,10 +80,11 @@ const LEGACY_FORM_TITLES = new Set([
]);
function normalizeFormSchemaTitle(schema: FormSchema): FormSchema {
if (!LEGACY_FORM_TITLES.has(schema.title) && schema.title.trim().length > 0) {
return schema;
const normalized = normalizeFormSchema(schema);
if (!LEGACY_FORM_TITLES.has(normalized.title) && normalized.title.trim().length > 0) {
return normalized;
}
return { ...schema, title: DEMO_SCHEMA.title };
return { ...normalized, title: DEMO_SCHEMA.title };
}
function parseCaptureState(raw: unknown): CaptureStateSnapshot | null {
@ -108,7 +130,11 @@ export function saveCaptureState(
): void {
if (typeof storage?.setItem !== "function") return;
try {
storage.setItem(captureStateKey(sessionId), JSON.stringify(state));
const normalized: CaptureStateSnapshot = {
...state,
formSchema: normalizeFormSchema(state.formSchema),
};
storage.setItem(captureStateKey(sessionId), JSON.stringify(normalized));
} catch (err) {
console.warn("saveCaptureState: write failed", err);
}
@ -140,4 +166,4 @@ export function removeCaptureState(
): void {
if (typeof storage?.removeItem !== "function") return;
storage.removeItem(captureStateKey(sessionId));
}
}

View file

@ -1,30 +1,33 @@
/**
* Demo attributes schema for Capture mode (CE-WP-0003 form-binding slice;
* CE-WP-0010 renames the user-facing concept from "form" to "attributes").
*
* Deliberately minimal: text, textarea, date. JSON Schema is **not** used
* here that's deferred to a later ADR. The schema's only job is to render
* a handful of key/value attributes and accept evidence links so the
* visual-guide round-trip can be exercised end-to-end.
* Demo attributes schema for Capture mode.
* CE-WP-0012: richer value types (amount, one-of, ) per accepted catalog defaults.
*/
export type FormFieldSchema =
| { readonly type: "text"; readonly id: string; readonly label: string }
| { readonly type: "textarea"; readonly id: string; readonly label: string }
| { readonly type: "date"; readonly id: string; readonly label: string };
import type { FormFieldSchema, FormSchema } from "@binder/FormRenderer";
export interface FormSchema {
readonly id: string;
readonly title: string;
readonly fields: readonly FormFieldSchema[];
}
export type { FormFieldSchema, FormSchema };
export const DEMO_SCHEMA: FormSchema = {
id: "demo-form",
title: "Attributes",
fields: [
{ type: "textarea", id: "summary", label: "Summary of the matter" },
{ type: "text-long", id: "summary", label: "Summary of the matter" },
{ type: "date", id: "deadline", label: "Key deadline" },
{ type: "text", id: "amount", label: "Disputed amount" },
{
type: "amount",
id: "amount",
label: "Disputed amount",
defaultCurrency: "EUR",
},
{
type: "one-of",
id: "payment",
label: "Payment method",
options: [
{ id: "cash", label: "Cash" },
{ id: "transfer", label: "Bank transfer" },
{ id: "other", label: "Other" },
],
},
],
};

View file

@ -84,7 +84,12 @@ describe("Capture session persistence", () => {
const user = userEvent.setup();
await loadApp();
await user.type(screen.getByRole("textbox", { name: /Disputed amount/ }), "EUR 500");
const amountInput = document.querySelector(
'#field-amount',
) as HTMLInputElement;
expect(amountInput).toBeTruthy();
await user.clear(amountInput);
await user.type(amountInput, "500");
await waitFor(() => {
const keys = Object.keys(globalThis.localStorage ?? {});
@ -92,7 +97,8 @@ describe("Capture session persistence", () => {
expect(captureKey).toBeTruthy();
const sessionId = captureKey!.split(":")[2];
const state = loadCaptureState(sessionId as never);
expect(state?.fieldValues.amount).toBe("EUR 500");
expect(state?.fieldValues.amount).toContain("500");
expect(state?.fieldValues.amount).toContain("EUR");
expect(captureStateKey(sessionId as never)).toBe(captureKey);
});
},

View file

@ -1,10 +1,18 @@
# Proposal: Attribute Value Type Canon (consumer + InfoTechCanon)
**Status:** research draft (2026-07-30)
**Status:** accepted defaults (2026-07-30) — CE MVP frozen
**Consumers:** citation-evidence Capture attributes
**Producer candidate:** info-tech-canon (Data Model extension)
**Related:** CE-WP-0012, ITC-WP-0013 (proposed)
### Accepted defaults (operator 2026-07-30)
1. **Owner:** InfoTechCanon Data Model (long-term); CE ships interim catalog.
2. **Amount wire form:** structured JSON `{"value":"…","currency":"EUR"}`.
3. **textarea:** accept on load; normalize to `text-long` on write.
4. **boolean / integer / number / uri / identifier:** reserved, not implemented yet.
5. **one-of UI:** native `<select>` (not radio list) for MVP.
---
## 1. Problem

View file

@ -17,6 +17,7 @@ planning_priority: high
spec_refs:
- workplans/CE-WP-0010-annotate-attributes-ux.md
- workplans/CE-WP-0003-form-binding-visual-guide.md
state_hub_workstream_id: "e88d43c4-6ad3-4144-aa13-6e33f241cd9d"
---
# CE-WP-0011 — Attributes guide polish
@ -45,6 +46,7 @@ this workplan covers residual UX and n:m visual guide behaviour.
id: CE-WP-0011-T01
status: done
priority: high
state_hub_task_id: "dd01c991-316f-4c7f-983e-c8990474fd53"
```
Normalize `formSchema.title` when loading capture-state from localStorage.
@ -55,6 +57,7 @@ Normalize `formSchema.title` when loading capture-state from localStorage.
id: CE-WP-0011-T02
status: done
priority: high
state_hub_task_id: "9df7737d-ba67-4659-a941-33489d47f45f"
```
Update `Overlay` to resolve all links via binding service; card right →
@ -66,6 +69,7 @@ field left.
id: CE-WP-0011-T03
status: done
priority: medium
state_hub_task_id: "ed7aa3d5-255f-4b2d-9ef3-945dc7cea4a0"
```
`FormRenderer` header keeps title + filter; add button and add form at bottom.
@ -77,6 +81,7 @@ id: CE-WP-0011-T04
status: done
priority: medium
depends_on: [T01, T02, T03]
state_hub_task_id: "b596cd0d-b3cc-4f50-9242-f0c3e41e99ae"
```
Unit/DOM coverage; mark workplan finished.

View file

@ -7,7 +7,7 @@ repo: citation-evidence
repo_id: a677c189-b4e2-4f2a-9e48-faa482c277e6
topic_slug: citation_evidence_mvp
topic_id: cee7bedf-2b48-46ef-8601-006474f2ad7a
status: proposed
status: finished
owner: codex
created: "2026-07-30"
updated: "2026-07-30"
@ -84,7 +84,7 @@ ship T01T06 against the wiki proposal and remap ids if ITC renames.
```task
id: CE-WP-0012-T01
status: todo
status: done
priority: high
```
@ -99,7 +99,7 @@ operator; freeze MVP wire forms for amount / one-of / some-of.
```task
id: CE-WP-0012-T02
status: todo
status: done
priority: high
depends_on: [T01]
```
@ -118,7 +118,7 @@ Extend `FormFieldSchema` / `FieldType` in evidence-binder:
```task
id: CE-WP-0012-T03
status: todo
status: done
priority: high
depends_on: [T02]
```
@ -134,7 +134,7 @@ Values persist as strings (or structured JSON for amount per T01).
```task
id: CE-WP-0012-T04
status: todo
status: done
priority: high
depends_on: [T02]
```
@ -151,7 +151,7 @@ depends_on: [T02]
```task
id: CE-WP-0012-T05
status: todo
status: done
priority: medium
depends_on: [T03, T04]
```
@ -166,7 +166,7 @@ depends_on: [T03, T04]
```task
id: CE-WP-0012-T06
status: todo
status: done
priority: medium
depends_on: [T05]
```