Skip to content

Commit 17ec338

Browse files
Hafeokclaude
andcommitted
fix(pf): align §11/§12 preview manifests with canonical v1.3.0 YAML schema
The framework's own example manifests are YAML with a richer schema than the TOML FT-141/142 first shipped; `product preview` could not read them. Reworked to the canonical shape so the tool validates the framework's examples verbatim: - design-system: nested under `design_system:`; `contexts_supported` is a {form_factor, modality} space; `reification[].when` is a partial predicate (unconstrained dimension = wildcard, `{}` = all); `satisfies[].via`; `tokens` are {id, type}. Coupling now checks every AIO the What's UI steps *reference* has a reifying CIO compatible with each declared context of use (§11.2 seam), matching `when` against a context's dimension/value. - content-store: nested under `content_store:` with `entries` inside; YAML. Parsing switched TOML→serde_yaml. Vendored the canonical checkout example manifests under examples/checkout/ as a live fixture. Synced docs/product-framework-open.md from v1.2 to v1.3.0 (§3.1 structure/data split, §6.3 data-conformance kind, §13 Data Conformance Profile). `product preview design-system|content-store examples/checkout/*.yaml` → whole. `product verify` → TC-1015/1016/1017/1018 PASS. Full suite green; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent d5877cd commit 17ec338

10 files changed

Lines changed: 395 additions & 149 deletions

File tree

.product/requests.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,5 @@
6363
{"applied-at":"2026-06-22T13:02:14Z","applied-by":"git:Emil Klein - Claude Code AI <[email protected]>","commit":"dbd1b0c","entry-hash":"f8b1da3a2952da40d666dc674d09149456b18afac3453afeb0f3cf445126d53c","feature":"FT-140","id":"req-20260622-001","prev-hash":"803d01d81968600b8cef0e79f5794208b04f9c7510feccd3eee5ab68e2d52e5a","reason":"verify FT-140: 3/3 passing","result":{"failing":[],"passing":["TC-1012","TC-1013","TC-1014"],"tag-created":"product/FT-140/complete","tcs-run":["TC-1012","TC-1013","TC-1014"]},"type":"verify"}
6464
{"applied-at":"2026-06-22T13:20:01Z","applied-by":"git:Emil Klein - Claude Code AI <[email protected]>","commit":"f457f25","entry-hash":"9e201e8cb8826354df33f72372322df8666c03184f0c0a90c105dbb8c008f236","feature":"FT-141","id":"req-20260622-002","prev-hash":"f8b1da3a2952da40d666dc674d09149456b18afac3453afeb0f3cf445126d53c","reason":"verify FT-141: 2/2 passing","result":{"failing":[],"passing":["TC-1015","TC-1016"],"tag-created":"product/FT-141/complete","tcs-run":["TC-1015","TC-1016"]},"type":"verify"}
6565
{"applied-at":"2026-06-22T13:23:55Z","applied-by":"git:Emil Klein - Claude Code AI <[email protected]>","commit":"f457f25","entry-hash":"94ec3f19bffc7829b60138726d316d1666c951e762dbce55592e1db78de0ab38","feature":"FT-142","id":"req-20260622-003","prev-hash":"9e201e8cb8826354df33f72372322df8666c03184f0c0a90c105dbb8c008f236","reason":"verify FT-142: 2/2 passing","result":{"failing":[],"passing":["TC-1017","TC-1018"],"tag-created":"product/FT-142/complete","tcs-run":["TC-1017","TC-1018"]},"type":"verify"}
66+
{"applied-at":"2026-06-22T18:49:07Z","applied-by":"git:Emil Klein - Claude Code AI <[email protected]>","commit":"d5877cd","entry-hash":"2432e827e4e970e5f668e19d175fa42c3c5e533847bb36a6c24e76c5117cdafa","feature":"FT-141","id":"req-20260622-004","prev-hash":"94ec3f19bffc7829b60138726d316d1666c951e762dbce55592e1db78de0ab38","reason":"verify FT-141: 2/2 passing","result":{"failing":[],"passing":["TC-1015","TC-1016"],"tag-created":"product/FT-141/complete-v2","tcs-run":["TC-1015","TC-1016"]},"type":"verify"}
67+
{"applied-at":"2026-06-22T18:49:09Z","applied-by":"git:Emil Klein - Claude Code AI <[email protected]>","commit":"d5877cd","entry-hash":"3a4db3fd135dc20af5cc5ffb730cdec8f0d58f68a713fa61662d486eb1fccc3f","feature":"FT-142","id":"req-20260622-005","prev-hash":"2432e827e4e970e5f668e19d175fa42c3c5e533847bb36a6c24e76c5117cdafa","reason":"verify FT-142: 2/2 passing","result":{"failing":[],"passing":["TC-1017","TC-1018"],"tag-created":"product/FT-142/complete-v2","tcs-run":["TC-1017","TC-1018"]},"type":"verify"}

docs/product-framework-open.md

Lines changed: 57 additions & 5 deletions
Large diffs are not rendered by default.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# content-store.manifest.yaml (PREVIEW — exercises §12)
2+
# A conforming content store for the checkout example. Resolves every content key
3+
# the What references, across the locales it claims (§12.1: keyed catalog with
4+
# roles, locale coverage, role conformance). Demonstrates "one What, many words".
5+
6+
content_store:
7+
id: "wire-content"
8+
version: "0.1.0"
9+
locales_supported: [en, es]
10+
11+
entries:
12+
- key: checkout.review.heading
13+
role: heading
14+
values:
15+
en: "Review your order"
16+
es: "Revisa tu pedido"
17+
18+
- key: checkout.payment.heading
19+
role: heading
20+
values:
21+
en: "How would you like to pay?"
22+
es: "¿Cómo quieres pagar?"
23+
24+
- key: confirm.heading
25+
role: heading
26+
values:
27+
en: "You're all set"
28+
es: "Todo listo"
29+
30+
- key: browse.heading
31+
role: heading
32+
values:
33+
en: "Browse the shop"
34+
es: "Explora la tienda"
35+
36+
- key: orders.heading
37+
role: heading
38+
values:
39+
en: "Your orders"
40+
es: "Tus pedidos"
41+
42+
- key: cart.empty.message
43+
role: empty-message # role: must be non-empty and actionable
44+
values:
45+
en: "Nothing to check out yet — add something to get started."
46+
es: "Aún no hay nada para pagar — añade algo para empezar."
47+
48+
- key: cart.failed.message
49+
role: error-message # role: must say what went wrong and how to recover
50+
values:
51+
en: "Couldn't load your cart. Check your connection and retry."
52+
es: "No se pudo cargar tu carrito. Revisa tu conexión e inténtalo de nuevo."
53+
54+
# Coverage check (§12.2): every content key referenced by the What's UI steps and
55+
# shell resolves in both `en` and `es`; every empty-/error-message role is non-empty.
56+
# A renderer coupling this store with locale=es produces the same screens in Spanish,
57+
# with no change to the What.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# design-system.manifest.yaml (PREVIEW — exercises §11)
2+
# A conforming design system for the checkout example. Declares the four
3+
# obligations of §11.1: a CIO catalog, reification coverage, a token surface,
4+
# and per-component WCAG guarantees.
5+
6+
design_system:
7+
id: "wire-ds"
8+
version: "0.1.0"
9+
wcag_target: "2.2-AA"
10+
11+
contexts_supported:
12+
form_factor: [phone, tablet]
13+
modality: [touch, pointer]
14+
15+
components: # the CIO catalog
16+
- id: searchable-list
17+
tokens: [color.fg, color.bg, space.inset, type.body]
18+
satisfies:
19+
- { criterion: "1.3.1", level: A, via: machine }
20+
- { criterion: "4.1.2", level: A, via: machine }
21+
- { criterion: "2.4.7", level: AA, via: assisted }
22+
- id: segmented-control
23+
tokens: [color.fg, color.bg, space.inset]
24+
satisfies:
25+
- { criterion: "1.3.1", level: A, via: machine }
26+
- { criterion: "4.1.2", level: A, via: machine }
27+
- id: list
28+
tokens: [color.fg, space.inset, type.body]
29+
satisfies:
30+
- { criterion: "1.3.1", level: A, via: machine }
31+
- id: value-block
32+
tokens: [color.fg, type.display]
33+
satisfies:
34+
- { criterion: "1.3.1", level: A, via: machine }
35+
- id: primary-button
36+
tokens: [color.accent, color.on-accent, space.inset, type.label]
37+
satisfies:
38+
- { criterion: "2.5.8", level: AA, via: machine } # target size
39+
- { criterion: "2.4.7", level: AA, via: assisted }
40+
- id: drawer # the burger menu — root navigation reified
41+
tokens: [color.bg, color.fg, space.inset]
42+
satisfies:
43+
- { criterion: "2.4.7", level: AA, via: assisted }
44+
- { criterion: "2.1.1", level: A, via: manual } # keyboard operable
45+
- id: rail # tablet root navigation
46+
tokens: [color.bg, color.fg]
47+
satisfies:
48+
- { criterion: "2.4.7", level: AA, via: assisted }
49+
50+
reification: # reify(AIO, context) -> CIO ; must cover every pair
51+
- { aio: single-select, when: { form_factor: phone, options: many }, cio: searchable-list,
52+
rationale: "no room for many side-by-side options on a phone" }
53+
- { aio: single-select, when: { form_factor: tablet, options: few }, cio: segmented-control,
54+
rationale: "few options, ample width — direct choice beats a menu" }
55+
- { aio: display-collection, when: {}, cio: list }
56+
- { aio: display-value, when: {}, cio: value-block }
57+
- { aio: trigger-action, when: { emphasis: primary }, cio: primary-button }
58+
- { aio: navigate, when: { scope: root, form_factor: phone }, cio: drawer,
59+
rationale: "global destinations behind a drawer on a phone" }
60+
- { aio: navigate, when: { scope: root, form_factor: tablet }, cio: rail }
61+
62+
tokens: # the token surface
63+
- { id: color.fg, type: color }
64+
- { id: color.bg, type: color }
65+
- { id: color.accent, type: color }
66+
- { id: color.on-accent, type: color }
67+
- { id: space.inset, type: dimension }
68+
- { id: type.body, type: typography }
69+
- { id: type.label, type: typography }
70+
- { id: type.display, type: typography }

product-cli/src/commands/preview.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ fn content_store(manifest: PathBuf, couple: bool, product: Option<String>) -> Bo
6060
findings.extend(product_core::pf::manifest_content::couple_content(&m, &graph));
6161
}
6262

63+
let cs = &m.content_store;
6364
let summary = format!("content store '{}': {{scope}} — {} entries, {} locales",
64-
m.content_store.id, m.entries.len(), m.content_store.locales_supported.len());
65-
report(&format!("content store '{}'", m.content_store.id), couple, &summary, findings)
65+
cs.id, cs.entries.len(), cs.locales_supported.len());
66+
report(&format!("content store '{}'", cs.id), couple, &summary, findings)
6667
}
6768

6869
/// Print the scope line (whole / whole + coupled) on success, or each finding on
@@ -91,9 +92,10 @@ fn design_system(manifest: PathBuf, couple: bool, product: Option<String>) -> Bo
9192
findings.extend(product_core::pf::manifest::couple_ds(&m, &graph));
9293
}
9394

95+
let ds = &m.design_system;
9496
let summary = format!("design system '{}': {{scope}} — {} components, {} reification rules",
95-
m.design_system.id, m.components.len(), m.reification.len());
96-
report(&format!("design system '{}'", m.design_system.id), couple, &summary, findings)
97+
ds.id, ds.components.len(), ds.reification.len());
98+
report(&format!("design system '{}'", ds.id), couple, &summary, findings)
9799
}
98100

99101
/// Load the captured What graph for the coupling check.

product-cli/tests/integration/mod.rs

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9803,27 +9803,38 @@ fn tc_1014_seam_composes_coverage_failures() {
98039803
}
98049804
}
98059805

9806-
/// A whole §11.3 design-system manifest reifying every core AIO on `phone`.
9806+
/// A §11.3 design-system manifest (canonical YAML) with the given reification
9807+
/// rules (each `(aio, when, cio)`). `whole_ds_manifest` reifies single-select on
9808+
/// phone and trigger-action by emphasis.
9809+
fn ds_manifest(rules: &[(&str, &str, &str)]) -> String {
9810+
let reify: String = rules.iter().map(|(aio, when, cio)| format!(
9811+
" - {{ aio: {aio}, when: {{ {when} }}, cio: {cio}, rationale: x }}\n")).collect();
9812+
format!(
9813+
"design_system:\n id: acme\n version: \"1.0\"\n wcag_target: \"2.2-AA\"\n\
9814+
\x20 contexts_supported: {{ form_factor: [phone, tablet], modality: [touch] }}\n\
9815+
\x20 components:\n\
9816+
\x20 - {{ id: searchable-list, tokens: [color.accent], satisfies: [{{ criterion: \"1.3.1\", level: A, via: machine }}] }}\n\
9817+
\x20 - {{ id: primary-button, tokens: [color.accent], satisfies: [{{ criterion: \"2.5.8\", level: AA, via: machine }}] }}\n\
9818+
\x20 reification:\n{reify}\
9819+
\x20 tokens: [{{ id: color.accent, type: color }}]\n")
9820+
}
9821+
98079822
fn whole_ds_manifest() -> String {
9808-
let aios = ["trigger-action", "single-select", "multi-select", "text-entry", "numeric-entry",
9809-
"date-entry", "display-value", "display-collection", "navigate", "edit"];
9810-
let rules: String = aios.iter().map(|a| format!(
9811-
"[[reification]]\naio = \"{a}\"\nwhen = \"phone\"\ncio = \"segmented-control\"\nrationale = \"x\"\n")).collect();
9812-
format!("[design_system]\nid = \"acme\"\nversion = \"1.0\"\nwcag_target = \"AA\"\n\
9813-
contexts_supported = [\"phone\"]\ntokens = [\"color.accent\"]\n\
9814-
[[components]]\nid = \"segmented-control\"\ntokens = [\"color.accent\"]\n\
9815-
satisfies = [{{ criterion = \"1.3.1\", level = \"A\", verification = \"machine\" }}]\n{rules}")
9823+
ds_manifest(&[
9824+
("single-select", "form_factor: phone", "searchable-list"),
9825+
("trigger-action", "emphasis: primary", "primary-button"),
9826+
])
98169827
}
98179828

98189829
#[test]
98199830
fn tc_1015_design_system_manifest_validates_internally() {
98209831
let h = Harness::new_bare();
98219832
h.run(&["init", "--yes", "--name", "shop", "--demo"]).assert_exit(0);
9822-
h.write("ds.toml", &whole_ds_manifest());
9823-
h.run(&["preview", "design-system", "ds.toml"]).assert_exit(0);
9833+
h.write("ds.yaml", &whole_ds_manifest());
9834+
h.run(&["preview", "design-system", "ds.yaml"]).assert_exit(0);
98249835
// A reification naming a cio absent from components fails wholeness.
9825-
h.write("bad.toml", &whole_ds_manifest().replacen("cio = \"segmented-control\"", "cio = \"ghost-cio\"", 1));
9826-
let out = h.run(&["preview", "design-system", "bad.toml"]);
9836+
h.write("bad.yaml", &whole_ds_manifest().replacen("cio: searchable-list", "cio: ghost-cio", 1));
9837+
let out = h.run(&["preview", "design-system", "bad.yaml"]);
98279838
out.assert_exit(1);
98289839
assert!(out.stderr.contains("ghost-cio") && out.stderr.contains("absent"), "stderr:\n{}", out.stderr);
98299840
}
@@ -9832,42 +9843,45 @@ fn tc_1015_design_system_manifest_validates_internally() {
98329843
fn tc_1016_design_system_coupling_covers_every_aio_context() {
98339844
let h = Harness::new_bare();
98349845
h.run(&["init", "--yes", "--name", "shop", "--demo"]).assert_exit(0);
9835-
h.run(&["domain", "new", "context-of-use", "phone", "--label", "P"]).assert_exit(0);
9836-
h.write("ds.toml", &whole_ds_manifest());
9837-
// Full reification over phone → coupling complete.
9838-
h.run(&["preview", "design-system", "ds.toml", "--couple"]).assert_exit(0);
9846+
h.run(&["domain", "new", "context-of-use", "phone", "--label", "P",
9847+
"--dimension", "form_factor", "--value", "phone"]).assert_exit(0);
9848+
// A UI step referencing single-select (surfaced) + trigger-action (offered),
9849+
// against the demo's existing read model + command.
9850+
h.run(&["domain", "new", "ui-step", "Pick", "--label", "Pick",
9851+
"--surfaces", "OrderSummary:single-select", "--offers", "PlaceOrder:trigger-action"]).assert_exit(0);
9852+
h.write("ds.yaml", &whole_ds_manifest());
9853+
// Both referenced AIOs reify on phone → coupling complete.
9854+
h.run(&["preview", "design-system", "ds.yaml", "--couple"]).assert_exit(0);
98399855
// Drop single-select's rule → non-conforming for phone, naming the gap.
9840-
let dropped = whole_ds_manifest().replacen(
9841-
"[[reification]]\naio = \"single-select\"\nwhen = \"phone\"\ncio = \"segmented-control\"\nrationale = \"x\"\n", "", 1);
9842-
h.write("gap.toml", &dropped);
9843-
let out = h.run(&["preview", "design-system", "gap.toml", "--couple"]);
9856+
h.write("gap.yaml", &ds_manifest(&[("trigger-action", "emphasis: primary", "primary-button")]));
9857+
let out = h.run(&["preview", "design-system", "gap.yaml", "--couple"]);
98449858
out.assert_exit(1);
98459859
assert!(out.stderr.contains("single-select") && out.stderr.contains("phone"), "stderr:\n{}", out.stderr);
98469860
}
98479861

9848-
/// A whole §12.2 content-store manifest with two entries over en/de.
9862+
/// A whole §12.2 content-store manifest (canonical YAML) with two entries over en/de.
98499863
fn whole_content_manifest() -> String {
9850-
"[content_store]\nid = \"copy\"\nversion = \"1.0\"\nlocales_supported = [\"en\", \"de\"]\n\
9851-
[[entries]]\nkey = \"cart.empty.message\"\nrole = \"empty-message\"\n\
9852-
values = { en = \"Your cart is empty\", de = \"Ihr Warenkorb ist leer\" }\n\
9853-
[[entries]]\nkey = \"checkout.title\"\nrole = \"heading\"\n\
9854-
values = { en = \"Checkout\", de = \"Kasse\" }\n".to_string()
9864+
"content_store:\n id: copy\n version: \"1.0\"\n locales_supported: [en, de]\n entries:\n\
9865+
\x20 - key: cart.empty.message\n role: empty-message\n\
9866+
\x20 values: { en: \"Your cart is empty\", de: \"Ihr Warenkorb ist leer\" }\n\
9867+
\x20 - key: checkout.title\n role: heading\n\
9868+
\x20 values: { en: \"Checkout\", de: \"Kasse\" }\n".to_string()
98559869
}
98569870

98579871
#[test]
98589872
fn tc_1017_content_store_manifest_validates_internally() {
98599873
let h = Harness::new_bare();
98609874
h.run(&["init", "--yes", "--name", "shop", "--demo"]).assert_exit(0);
9861-
h.write("cs.toml", &whole_content_manifest());
9862-
h.run(&["preview", "content-store", "cs.toml"]).assert_exit(0);
9875+
h.write("cs.yaml", &whole_content_manifest());
9876+
h.run(&["preview", "content-store", "cs.yaml"]).assert_exit(0);
98639877
// A key missing a value for a claimed locale fails wholeness.
9864-
h.write("nolocale.toml", &whole_content_manifest().replacen(", de = \"Kasse\"", "", 1));
9865-
let a = h.run(&["preview", "content-store", "nolocale.toml"]);
9878+
h.write("nolocale.yaml", &whole_content_manifest().replacen(", de: \"Kasse\"", "", 1));
9879+
let a = h.run(&["preview", "content-store", "nolocale.yaml"]);
98669880
a.assert_exit(1);
98679881
assert!(a.stderr.contains("checkout.title") && a.stderr.contains("de"), "stderr:\n{}", a.stderr);
98689882
// An error/empty-message role resolving to empty text fails.
9869-
h.write("empty.toml", &whole_content_manifest().replacen("Your cart is empty", "", 1));
9870-
let b = h.run(&["preview", "content-store", "empty.toml"]);
9883+
h.write("empty.yaml", &whole_content_manifest().replacen("Your cart is empty", "", 1));
9884+
let b = h.run(&["preview", "content-store", "empty.yaml"]);
98719885
b.assert_exit(1);
98729886
assert!(b.stderr.contains("cart.empty.message") && b.stderr.contains("empty"), "stderr:\n{}", b.stderr);
98739887
}
@@ -9878,12 +9892,12 @@ fn tc_1018_content_store_coupling_resolves_every_referenced_key() {
98789892
h.run(&["init", "--yes", "--name", "shop", "--demo"]).assert_exit(0);
98799893
h.run(&["domain", "new", "ui-step", "Cart", "--label", "Cart",
98809894
"--content", "cart.empty.message:empty-message"]).assert_exit(0);
9881-
h.write("cs.toml", &whole_content_manifest());
9895+
h.write("cs.yaml", &whole_content_manifest());
98829896
// The store resolves the referenced key in every locale → coupling complete.
9883-
h.run(&["preview", "content-store", "cs.toml", "--couple"]).assert_exit(0);
9897+
h.run(&["preview", "content-store", "cs.yaml", "--couple"]).assert_exit(0);
98849898
// A UI step referencing an unresolved key → non-conforming for that locale.
98859899
h.run(&["domain", "new", "ui-step", "P2", "--label", "P2", "--content", "missing.key:body"]).assert_exit(0);
9886-
let out = h.run(&["preview", "content-store", "cs.toml", "--couple"]);
9900+
let out = h.run(&["preview", "content-store", "cs.yaml", "--couple"]);
98879901
out.assert_exit(1);
98889902
assert!(out.stderr.contains("missing.key") && (out.stderr.contains("de") || out.stderr.contains("en")), "stderr:\n{}", out.stderr);
98899903
}

0 commit comments

Comments
 (0)