Implement CE-WP-0012 attribute types; freeze accepted catalog defaults.
Demo schema exercises amount and one-of; capture-state validates options; proposal marked accepted; workplan finished.
This commit is contained in:
parent
d4327b6be6
commit
36d8d3a73f
9 changed files with 117 additions and 56 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 T01–T06 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]
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue