mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
Replace the invented area axis with grain and platform
The area taxonomy was wrong, and wrong in a way worth recording. It was derived from Mobbin-style categories in the abstract rather than from what the skill can be asked for, and measured against the catalog most of it described problems that were not there: onboarding, settings, empty-state and search each had zero entries. Reframed against demand instead. A user asks for a docs site, an onboarding flow, a landing page, or a data table, and those differ in how much of the product is in play. Register already says what kind of work it is; grain says how much: product, flow, view, region. Named grain rather than scope because scope already means direction-or-surface on every roll and 'surface' is already a register value, so a scope of 'surface' would have collided with both. Platform is the second axis: web, ios, android. Unlike grain it is a hard filter with no fallback, because a composition that leans on hover or a pointer does not degrade on a phone into something slightly worse, it stops working, and an empty deal is a visible gap where a broken one is not. Both fields are optional and absence means eligible everywhere, so nothing needs backfilling and no existing roll changes. The third piece is the one a trace turned up. Asking for an onboarding flow resolves to register=operate, grain=flow, and the catalog holds zero flow-grain compositions, so the top-up would have dealt three plausible single-screen compositions with no signal that none matched. The model would have improvised the flow structure while believing it was handed one, which is the same silent plausibility the axis exists to remove. Selection now returns a match alongside the picks, and the rendered seed says when the structure is borrowed and why. Measured at the time of writing: 137 of 173 approved compositions are view grain, product grain is empty, flow grain holds one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
48e665edf2
commit
86b91a2003
@@ -3,9 +3,9 @@ import { readFileSync } from 'node:fs';
|
||||
import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs';
|
||||
// Defined in roll-selection.mjs for the same reason WELL_TIERS is: this file
|
||||
// reads the filesystem, and the roll API imports the taxonomy to validate its
|
||||
// area parameter. Re-exported so existing importers keep working.
|
||||
import { areasForSurface, COMPOSITION_AREAS, ALL_COMPOSITION_AREAS } from './roll-selection.mjs';
|
||||
export { areasForSurface, COMPOSITION_AREAS, ALL_COMPOSITION_AREAS };
|
||||
// grain and platform parameters. Re-exported so importers have one place to look.
|
||||
import { COMPOSITION_GRAINS, COMPOSITION_PLATFORMS, isGrain, isPlatform } from './roll-selection.mjs';
|
||||
export { COMPOSITION_GRAINS, COMPOSITION_PLATFORMS, isGrain, isPlatform };
|
||||
|
||||
// Catalog B: compositions rather than styles. A composition organizes attention,
|
||||
// sequence, or manipulation on a surface and must survive being dressed in
|
||||
@@ -63,14 +63,24 @@ export function validateCompositionEntry(composition, { existingForms = new Map(
|
||||
if (!COMPOSITION_SURFACES.has(composition?.surface)) {
|
||||
errors.push(`composition ${id} needs a surface of ${[...COMPOSITION_SURFACES].join(', ')}`);
|
||||
}
|
||||
// Optional, but an area from the wrong surface is a mistake rather than a
|
||||
// looser tag: it would make the entry unreachable by every real request.
|
||||
if (composition?.area !== undefined && composition.area !== null) {
|
||||
const allowed = areasForSurface(composition.surface);
|
||||
if (!allowed.includes(composition.area)) {
|
||||
errors.push(
|
||||
`composition ${id} area "${composition.area}" is not one of the ${composition.surface} areas (${allowed.join(', ')})`
|
||||
);
|
||||
// Grain: how much of the product this composes. Optional, and absence means
|
||||
// eligible at any grain, so nothing needs backfilling.
|
||||
if (composition?.grain !== undefined && composition.grain !== null && !isGrain(composition.grain)) {
|
||||
errors.push(`composition ${id} grain "${composition.grain}" must be one of ${COMPOSITION_GRAINS.join(', ')}`);
|
||||
}
|
||||
// Platforms this composition survives. Absence means all of them, so listing
|
||||
// every platform is the same as omitting the field and is rejected in favour of
|
||||
// leaving it out; an empty array would exclude the entry from every roll.
|
||||
if (composition?.platforms !== undefined && composition.platforms !== null) {
|
||||
const list = composition.platforms;
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
errors.push(`composition ${id} platforms must be a non-empty array, or omitted to allow every platform`);
|
||||
} else if (list.some(entry => !isPlatform(entry))) {
|
||||
errors.push(`composition ${id} platforms may only contain ${COMPOSITION_PLATFORMS.join(', ')}`);
|
||||
} else if (new Set(list).size !== list.length) {
|
||||
errors.push(`composition ${id} platforms must not repeat a platform`);
|
||||
} else if (list.length === COMPOSITION_PLATFORMS.length) {
|
||||
errors.push(`composition ${id} platforms lists every platform; omit the field instead`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(composition?.tags)
|
||||
|
||||
Reference in New Issue
Block a user