Feature: Model authoring - #123
Merged
Merged
Conversation
The declared-manifest path — the one that makes a model definition data — was also the one path that silently dropped interpretation hints and the identity dedup key. PropertySchema/EntitySchema now carry them, the compiler threads them through the same decorators hand-written models use, the generator emits them, and the golden test compares them so a future miss fails loudly instead of surfacing as 'the model extracted nothing'. Also adds manifest-level 'options' (closed value sets) for forms and prompts, an at-most-one-identity validation rule, and fixes 'abstract' being stripped by the Zod schema on validated manifests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The executor reads hints off the shapes stored in the perspective, so the space was always the natural override point — but shapeIsStale rewrote any stored shape that differed from the class declaration, reverting a tuned hint on the next space switch. A we://interpretation_customized marker on the shape node now records the community's decision: the staleness check yields on hints (class and property) for marked shapes while structure — paths and the identity dedup key — stays code-owned and compared as before. New SchemaPort surface (interpretationHints / setInterpretationHints / resetInterpretationHints, keyed by predicate) implemented on both backends; reset rewrites the declared hints and clears the marker so release improvements flow again. Known v1 edge, documented in interpretationHints.ts: a structural refresh rewrites the whole shape graph and drops customizations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
moduleRegistry handed definition.entities.manifest straight to schemas.declare(), so a malformed manifest registered fine and failed on the first dataset switch, far from the module that shipped it. Refused at registration instead, through the same loud path as predicate violations — a gate that must be unbypassable before user-authored manifests share this compile path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Shape record is one content model a space carries: scalar metadata (name, icon, stable shapeId identity, forkedFrom lineage, version) plus the ModelManifest JSON in a file-storage property — the Template storage precedent applied to models. Definitions are stored with every predicate and type flag already resolved, so a reading peer compiles them without any minting rule and a rename can never re-mint the predicates existing data lives under. Substrate to carry it: validateManifest accepts externalEntities so a shape may target core vocabulary and sibling shapes; compileManifest accepts resolveExternal for the same reach at compile time; a new SchemaPort.declareInDataset registers compiled classes per dataset (mergeDynamicModels — additive, unlike the foreign-schema sweep's replace) so a shape resolves in its own space and nowhere else, with native classes still shadowing everything. Shape installs with the space SDNA set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On every switch into a WE space, Shape records are read, their manifest documents validated through the same gate module manifests pass (against core vocabulary, foreign entities and sibling shapes), compiled and registered for that dataset only, and their SHACL ensured into the space — after which $query, forms and extraction see the entity through machinery that already existed. Invalid definitions surface as per-shape problems rather than vanishing. The store also owns both editing surfaces' state: the wizard draft (shared by the structured editor and, next, the LLM flow — one review path) with the v1 additive-only edit guard and predicate preservation from shared/shapes/shapeDraft.ts, and the hint editor with the customized/reset lifecycle over SchemaPort.interpretationHints. Classified space-settings in the capability tiers: defining what a Sighting is here is the community shaping its own container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Models card in the default template's settings route: the space's own models listed with their adoption problems visible, a New model wizard (name/icon/description, class-level AI hint, property rows over the closed type set — text/number/boolean/date/select/reference — with required, identity, per-property hints, defaults, and reference targets picked from what this space can name), and per-space AI-hint tuning for core vocabulary and space shapes alike, with the customized badge and reset-to-defaults lifecycle. All form state binds to shapeStore (the aiForm precedent) so the LLM flow lands in the identical review path; the only $localState is the delete confirmation. shapeStore documented in the ai-context store entries so the schema validator and CLAUDE.md know it; draft option lists become one comma-separated string field, since a form bound to two representations of one value is how it drifts from what it saves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generateShapeDraft turns a plain-language description into the same ShapeDraft the wizard edits, via one forced define_model tool call whose schema mirrors the draft — the closed type set is unrepresentable to get wrong. The candidate is lowered and gated exactly as a hand-built draft (draftToManifest + validateManifest + name-collision check); refusals go back to the model as tool_result errors for up to two repair turns, and whatever survives lands in the open wizard with any remaining problems displayed. Generation proposes, the human saves — nothing reaches the space without the same review path the structured editor uses. Offered for new models only (a generated draft would replace stored predicates wholesale on an edit) and gated on the agent's Claude API key. Lives beside aiInfra as part of the same browser-calls-a-model surface a backend-executed assistant would replace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New model refused to open: a schema $action with no declared args passes the DOM event as the first argument, so openShapeWizard read a MouseEvent as a record id, found no such shape, and toasted. Anything but a real id now means 'new model'. Typing dropped focus after every character: the renderer keys $each rows by object reference and captures context refs as plain values, so the immutable-replace setters remounted the very input being typed in on each keystroke. Typed-field edits (names, hints, defaults, options) now mutate the draft in place — the DOM already shows the text, and save() reads the mutated state — while discrete edits (type, switches, targets) keep the replace so a row's conditional inputs re-render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…by drag The row was one form with a 'reference' pseudo-type that hid half its own inputs — a tagged union pretending not to be one, when EntitySchema has carried properties and relations as separate maps all along. Now two add buttons and two row shapes: a property has type/required/hint/default, a relationship has a target and one-or-many and nothing else. Drag-to-reorder, because declaration order is the order the manifest stores and will be the field order of the derived creation form. That needed two fixes in we-sortable first, both no-ops for an item without form controls: an optional [data-we-handle] grab area (otherwise dragging to select text starts a drag), and a rule that Space or Enter arriving from a text-entry element is typing rather than a pickup — without which a hint field could not accept spaces at all, since the keyboard pickup would swallow them. Also from testing the wizard: - Optional/Required on the switch, which had the same word on both sides. - Identity is a single picker below the fields, not a per-row toggle: at most one member can be it, and N switches can express a violation the save then has to refuse, where one picker cannot. Keyed by a draft-local row id so renaming or reordering the chosen field keeps the choice. - Delete pinned to the row's corner. It was wrapping onto its own line because the switch group claimed the slack; anchoring it there also stops it moving as a row's conditional inputs change. - The relationship target picker is grouped and labelled — this space's models, then block types, then other apps' — and no longer offers core infrastructure (Template, Theme, AgentSettings) that no community shape should point at. This is also where 'add a photo' becomes discoverable. And a guard the predicate check missed: a surviving predicate can still be redefined out from under its data (count: number edited to count: text keeps the storage key and changes how every stored value reads). additiveViolations now compares what each predicate means — kind, scalar type, relation target, and many→one — allowing one→many as the one widening that needs no migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
size drove the control's height and nothing else: the trigger's label and placeholder pinned --we-font-size-400 in the shadow CSS, and its preview and caret icons hardcoded size="sm". A picker asked for at sm came out the right height holding full-size text. Brought in line with its siblings — we-select, we-input and we-date-picker all pair the height map with a SIZE_DEFAULTS type scale merged through getInstanceProps, and sized primitives set --we-context-icon-size per :host([size=…]) so nested icons follow. size now reflects, which is what those host rules match on; md is unchanged, so nothing that did not ask for a size moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lacing it A description tells you how to fill a field in, so it has to be read before you do; an error is a reaction to what you already did. They sat in one slot rendered as 'error ? error : description', which put the instructions after the control and — worse — deleted them the moment the reader got the value wrong. aria-describedby already named both ids, so the markup was the only thing insisting they were alternatives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… labels Description and AI hint sat next to each other as near-identical textareas whose placeholders paraphrased their own labels, so nothing said why there were two. The explanation moves into each field's description — where it persists while you type and is read out with the control — and the placeholders become worked examples that differ in grammatical register: a noun phrase for what the record is, an instruction for when AI should make one. The hint example carries an include-then-exclude clause, which is the shape a workable hint needs and is easier to copy than to explain. Examples are book recommendations throughout rather than bird sightings: an AI hint only matters when extraction runs over conversations, so the example has to be something people actually say to each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three parts were font-size 300 — 'body' — so a field's label rendered at the same size as its own help text and only colour separated them. Worse, a form-field label sat a step larger than the we-text variant='label' beside it: both spellings appear ~45 times each across the templates, often in one view, so the app carried two label sizes. Taken from we-text's own variants rather than invented: label is 'label' (200 + medium, the weight it already had), description and error are 'footnote' (100), with the error weighted so it still carries at the smaller size. The values are pinned in the tests against the component's own stylesheet, since jsdom resolves neither the cascade nor custom properties and a computed style there would only report that a var is a var. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Model name must be a single identifier" reads as "one word only",
when multi-word names are the normal case and need nothing but their
spaces closing up — which is exactly the misreading that put a space in
the wizard's own placeholder. The message now does the transformation and
names the result: 'Model names run words together, each capitalised — try
"BookRecommendation" instead of "Book Recommendation"', and the same
for property and relationship names in camelCase ('due date' → 'dueDate').
An empty field is asked for a name rather than offered a suggestion, and
a name starting with a digit — which joining words cannot rescue — gets
the rule stated rather than a suggestion that would be refused again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… a gutter Three fixes from testing the fields list. The drop indicator was never missing — it was painting behind the dialog. we-modal promotes itself into the browser's top layer via popover=manual, and no z-index reaches above the top layer, so the ghost and the drop line, both appended to document.body with z-index 9998/9999, rendered underneath it. What read as 'drag works but has no drop bar' was actually no ghost either; the feedback was the source row's 0.3 opacity. They now join the top layer the same way, feature-detected, with the UA popover defaults reset. The advice in z-index.ts is corrected too — picking a bigger token cannot win against the top layer. A property card was painted neutral-50, which is exactly what we-input fills itself with, so every field vanished into its own container. Surfaces take the surface step (neutral-0) and controls keep their recessed one; the relationship row keeps its primary-50 tint, which is a different step and so still reads. Overriding bg on the inputs would have been the wrong direction — hover, active and focus all move to neutral-75, so the override would have to be repeated four times per call site to stay coherent. The drag handle moves into a gutter beside the whole card rather than sitting inside its first line, sized to one control so it stays level with the first row as the card grows and shrinks with its conditional inputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three things from working in the fields list. A default value took the same text input whatever the property was, so 'many' for a number or 'yes' for a boolean were mistakes the save had to refuse. Each type now gets the control that cannot express the mistake: a number input, a date picker (which emits the YYYY-MM-DD the rest of the system reads), and a picker over the declared values for a select. A boolean's picker is three-valued, because unset is not false and the manifest distinguishes them. What is stored is unchanged — the draft holds text either way, coerced once at lowering. The property row hid six inputs of unequal importance on one line: a name, a type and required are structural and always relevant, while a hint, a default and a value list are secondary and usually empty. The first three stay on the row; the rest move behind a caret, where there is room to label them and to give the hint the textarea the class-level hint already had. A row opens when it is added, when a model is generated, and whenever a validation error names it — which is why draftToManifest now reports the rows a refusal is about, not just the sentences. Relationship rows keep their single line: a target and a cardinality is genuinely all there is, so there is nothing to put behind a caret. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
we-textarea passed rows through and then overrode it in CSS: a hard min-height of 80px is about three lines, so rows below the default rendered at three and read as the prop being ignored. Floored at one control height instead. we-select's trigger was unclickable with nothing chosen. all:unset leaves the button no height of its own, so with no value and no placeholder it was full width and zero tall — the caret was the only hit area on the row. The :empty::before fallback written for exactly this never fired, because placeholder is a property on the host and attr() reads attributes. The button is stretched and centred, and the attribute is mirrored onto it. The number default used we-number-input, whose stepper starts from a number the draft does not have: the field holds text, empty meaning unset, so plus did nothing until minus had coerced it, and once set there was no way back to unset. It is a we-input with type=number now — empty is expressible, the browser refuses letters, and no stepper state is involved. Every default control also takes a fixed width rather than filling the panel, and a placeholder, since a default is a short value. Default value moves above AI hint, so the panel reads allowed values → the value picked from them → the prose about the field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y out You found it: the wrapper a reveal builds sets overflow:hidden for the element's whole life, not for the animation's. The clip is what makes the effect work — a growing track has to hide what overflows it — but held on afterwards it silently cuts anything painting outside its own box, which is why inputs in the expandable panel lost their hover ring. A select's dropdown inside one would have been cut off the same way. Both renderers now clip while closed or moving and release once settled open. The other half of the same section: an omitted exitTransition now mirrors the enter in $if, which the operator documentation has always described and only $animate actually did. A panel that eased open vanished on close, and the workaround in a template was to restate the same config twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…value The type reads Boolean and the manifest stores default: true / false, so a picker offering Yes/No was the odd register out — and the admin setting a default is often the one writing that field's AI hint next, where the values named have to be the ones the model emits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A date could be set and never unset, so any optional date field was a one-way door. we-date-picker gains a clear button when it holds a value, emitting the same change event with an empty string. - Typed select values never reached the default picker. The row is mutated in place while typing so the input keeps focus, which means nothing hanging off the row can be reactive — the picker now reads its entries from a store memo keyed by rowId, so a commit updates it without remounting the row and stealing focus. - The native number spinners could not be themed and were the one control in a form that did not match. Hidden; we-number-input remains the control for when stepping is the point. - The seeded row played its opening animation on every open, because the draft and the expanded set were two separate writes: the wizard mounted collapsed and then expanded. Batched, so it is simply open. - Clicking the backdrop discarded a half-written model silently. The modal's close routes through a guard that asks first, and only when there is something to lose — a pristine wizard still closes at a touch. Policy in the store rather than a new prop on the primitive, since the store is what knows whether the draft is dirty. - A growing modal pushed its content off the screen, buttons included: maxHeight lands on the base but nothing bounded the overflow. It scrolls now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four components open a panel anchored to a trigger and did it three different ways. we-popover used the Popover API with Floating UI. we-icon-picker hand-rolled position:fixed from a rect read on open, which escapes clipping but has nothing watching the trigger, so the panel drifted away from it as soon as anything scrolled. we-select and we-date-picker used plain absolute positioning, which any ancestor with a non-visible overflow clips — a modal, a scroll area, a mid-animation reveal, which is most of where they are used. Making the modal scroll made that last one impossible to ignore. openFloatingPanel is the one mechanism: promote into the browser's top layer with the Popover API, so no z-index, overflow or stacking context can clip or hide it — including we-modal, which is itself in the top layer and cannot be beaten any other way — then anchor with Floating UI and keep it anchored with autoUpdate. A popover is not reparented, so keyboard handling, aria-activedescendant and shadow-scoped styles all keep working, which is what made this safe to retrofit. Degrades to the old positioning where the Popover API is missing. All four now call it, we-popover included: leaving the original as its own variant is how the divergence started. The z-index note is corrected again — those tokens are now a fallback, not the mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promoting a panel into the top layer also handed it the UA's own [popover] rules, and one of them beats inheritance: a panel that had always taken its text colour from the component around it got color: CanvasText instead. All three set their own background and none set a colour, so themed text went black on a themed surface. The helper resets colour and inset now, which is the same class of thing POPOVER_RESETS does for the drag ghost. And the number field lost its arrows: hiding the native spinners answered 'they look like browser defaults' by removing them rather than replacing them. we-number-input already draws design-system steppers — it just could not express 'no number', which is why the wizard was not using it. It can now: an empty value renders empty, clearing the field takes a value back, and stepping an empty field starts from the minimum (or zero when unbounded) instead of doing arithmetic on an empty string, which is what made one button work and the other appear dead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host carries the border and any width prop, but the row inside sized itself to its content — so a number input given a width drew its box at that width and left the space after the + button empty. The row fills the host now and the field takes the slack. 'No default' was truncating to 'No def' in a field three characters wide. 'None' is what this wizard already calls not-set in the identity picker, so both pickers and every placeholder now say it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One width was applied to every default control. A select needs room for its option labels and a date for its format, but a number input already shrink-wraps to its steppers and a few digits — stretched to 220px it just put a gap between the number and the buttons that belong to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removing the width did not shrink it: we-form-field lays its control out in a column, and a column stretches its children across the cross axis, so the input filled the row instead of sizing to itself. alignSelf: start is the opt-out — the control now takes the width of its own steppers and a few digits, whatever the size or type scale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
width:100% on the inner row was the reason it kept claiming a whole row. The host is an inline-flex box that shrink-wraps when given no width, and a percentage width against a shrink-to-fit parent resolves from the available space instead — so the row asked for everything going. As a flex item it grows into a width when there is one and reports its own content width when there is not, which is what both cases wanted. Also a guard test: a backtick inside a css template comment closes the template, and the parse error lands somewhere else entirely. That has bitten four times in this branch, always while writing a comment naming a property in backticks. Now it fails as itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… characters The width came from the input, as James spotted. A bare flex:1 leaves the field's intrinsic width to the input itself, and an input's is its size attribute — twenty characters, about 180px — which the host then shrink-wrapped around: 180 plus two 32px steppers is the 249px it kept drawing at. Replacing width:3em with flex:1 removed the only thing that had been overriding it. A definite basis (flex: 1 1 3em) is what the surrounding box measures instead, with min-width:0 so the input's own min-content cannot put it back. Still grows into a width where one is given, which is what the earlier fix was for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neither alignSelf: start on the host nor a definite flex basis inside the component stopped it stretching to the panel width, and the built bundle confirms both shipped — so the reasoning was wrong somewhere I could not find by reading. An explicit width is the one thing that reliably holds, and a number wants roughly its steppers plus a few digits. The two primitive changes stay: the row filling the host is what stops the gap after the + button when a width is given, and the definite basis is right regardless of what else is going on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ran it in a browser instead of reasoning about it again — we-preview boots the whole app over the in-memory backend and has playwright-core, so a throwaway harness rendering the wizard's own structure could report every box in the chain. Three things were true at once, which is why each fix from the outside failed: - The host computes display:flex, not the inline-flex its defaults declare, so in a form field's column it is block-level and fills the row. Only an inline style beats the design system's own sheet, so the width has to come from a DS prop on the consumer; a :host rule in the component was silently overridden, and has been removed rather than left there lying. - The field's intrinsic width is an input's twenty characters, which is what a shrink-to-fit ancestor measures. A flex basis did not help, because a growable item still contributes its max-content width. - The size attribute has no effect at all here, since all:unset above it leaves the UA sizing behind. An explicit width in ch does work, and taking it from the value means the field hugs a short number and widens for a long one: empty 108px, '5' 100px, '1234567890' 162px, none of them clipped. The hardcoded 124px is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A select fills its container by default and should keep doing so: option text is usually somebody's data, and a control that hugged it would be as wide as the longest space name in the list. But that is wrong for a handful of short known words — true/false, or the values a field itself declares — where 220px is mostly empty. fit measures the *widest option*, not the current one. Sizing from the selection would resize the control every time somebody picked something and shift everything beside it; measured in a browser, this holds at 55px across both an empty boolean picker and one showing True, and grows to 90px for longer options. The sizer stacks every label in one grid cell inside the button, hidden without being removed so it still contributes width while painting nothing and staying out of the accessibility tree. It sat beside the button first, which collapsed the button to nothing and clipped the very label it was there to size — visible immediately in a screenshot. The placeholder moves into a real span while we are here, replacing an :empty::before that could not carry ellipsis. The relationship target picker deliberately keeps its fixed width: its options are model names, which is exactly the case fit is wrong for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fitted we-select sat at its option width until the pointer arrived, then jumped to the full width of its container. Nothing about the element changes on hover — same inline style, same classes — so the cause is the cascade. Design system props reach the element as --we-select-* custom properties consumed by a generated stylesheet, and that sheet re-declares width in its own interaction rules. The :host([fit]) rule in this component's stylesheet therefore held until the first hover and lost from then on. So the width goes inline, in updated(), where the generated sheet cannot reach it. An explicit width still wins: fit is only the default-sizing opinion, and it is read back through getInstanceProps rather than off the element, since width is assigned by whoever mounts this and is not declared here. Measured in a browser rather than inferred — the same cascade is why an equivalent :host rule on we-number-input never applied at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odals scroll
A modal with more content than it had room for spilled down the page instead of
scrolling — the wizard, once it had enough properties to fill the screen.
Two causes, both measured in a browser rather than inferred.
The generated stylesheet emits the overflow longhands after the shorthand in the
same declaration block, so they replace it. Nothing ever sets --we-x-overflow-x
(the DS prop is `overflow`), which made the declaration invalid at computed-value
time and resolved to `visible`. That discarded the overflow prop for every
primitive that sets one, and beat any overflow rule a component wrote for itself,
since the generated sheet is adopted last. The longhands now fall back to the
shorthand they replace, via a {p} placeholder that resolves to the component's
variable prefix. we-scroll-area and we-menu get the values they always declared;
no menu is height-bounded, so nothing that used to be reachable is now clipped.
The modal then still could not be scrolled to the top: its base centred content
on the main axis, which does nothing while the box grows with its content, but
centres the *overflow* too once maxHeight clamps it — pushing the first field
above the top edge where no scrolling reaches. The host still centres the modal
in the viewport, which is the centring anybody actually sees.
Also gives we-date-picker's clear button somewhere to be: it sat flush against
the calendar icon, reading as one control, and a bare 14px glyph is a target most
people miss.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The five types covered the common cases and left three gaps where the control, not the model, was doing the limiting. **Long text.** `text` renders a single-line input, so a description or notes field was cramped by the widget. The IR now carries `multiline` on a property — a fact about the value in the same way `type` is, since paragraph prose is not the same kind of thing as a title whatever both compile to. Declared rather than guessed from the property's name, so a reopened model comes back as what its author built. **Date & time.** A calendar day and an instant are different facts — a birthday has no time, a shift start is meaningless without one — and RDF draws the same line (xsd:date vs xsd:dateTime), so the IR now does too. we-date-picker gains `showTime`: a time field in the panel, midnight rather than a guessed hour on a chosen day, the panel staying open on the day so the time is still reachable, and a time given before a day dated today. The compiler never reads the scalar type — AD4M links are untyped — so splitting them costs nothing there. **Image.** The IR has had `format: 'file'` and `readAs: 'dataUri'` all along, and the compiler binds them to the file-storage language; the wizard simply did not offer them. Its detail panel drops the default-value field for an image, since the value is a file somebody uploads and an empty box labelled "Default value" only invites the attempt. Deliberately not added: multi-select, which needs cardinality on properties (the IR has it only on relations); a person type, which needs an agent picker; and JSON, which the IR permits but which nothing can query into, SHACL cannot validate and the extraction hints cannot describe — structure that deserves to exist deserves a relationship to another model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the long-text, date/datetime and image property types added in 526d3f6, keeping only we-date-picker's showTime — which is the control the one remaining date type needs. Properties are scalars; content is a relation to a block. An image belongs to an ImageBlock, which carries alt text and dimensions and can be signalled on, commented on and drawn in the graph — none of which a URL in a string can do. The relationship picker already offers every block type, so an image property was not just inconsistent with that but redundant with something the wizard already shipped, by the poorer of the two routes. The rule is now written down beside the type list, since it is what explains the absences there. Long text changed exactly one thing — which widget renders — so it was a presentation flag in a neutral manifest, and `multiline` would have made `widget`, `placeholder` and `rows` easier to add after it. Presentation belongs to templates, which is the architecture; if a generated record editor later needs more than the scalar type, that belongs in a sidecar on the Shape record rather than in the IR. Splitting date from datetime bought nothing either: manifestCompiler never reads a scalar type, so nothing enforced the distinction, and it left authors one more decision to get wrong. One type now, with an optional time — a day chosen with no time stays a bare YYYY-MM-DD and only becomes an instant once somebody says which one, so the value records whether a time was meant instead of every date acquiring a midnight nobody chose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e the time row The clear button pushed the calendar icon out through the right-hand border. The display field is a flex item, so its automatic minimum size is its own content: it refused to shrink below the date it was showing and shoved everything beside it out of the box. Widening the clear button is what made it visible, but a long enough value would always have done it. The field now shrinks and truncates, and neither icon is what gives way when room runs short. The time row was built with all: unset, so it was a raw browser control sitting under a themed panel — and it reached for the neutral ramp where the panel around it uses role tokens, which are what follow a theme. It is now built the way we-input builds its field: same border, padding, hover and focus treatment, on role tokens throughout. Its native picker glyph is gone for the same reason we-input hides the native number spinners — the browser draws it for the OS scheme rather than the theme, so on a dark theme it was a black clock on a dark panel and no CSS reaches it. A themed button replaces it and opens the same picker through showPicker(), so nothing is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l their content only Two fixes from testing, one mechanism each. The time picker's popup was white with bright blue squares on a dark theme. The browser draws that popup itself, coloured for color-scheme and nothing else — no token reaches it, and the same goes for scrollbars and native dropdowns everywhere. Darkness is not a separate flag to keep in sync: a negative multiplier *is* the inversion of the lightness scale, so themeToStyle now derives color-scheme from it. Both theme paths flow through that function — the global applyThemeVars and the scoped per-space style — and color-scheme inherits through shadow boundaries, so it reaches the input the popup anchors to. A theme that never touches the multiplier stays silent and inherits the ambient scheme, which is right: it did not change the polarity. The modal scrolled everything, title and close button included. Scrolling [part='base'] was the first fix and wrong by one element — the close button is anchored to base, so it rode away with the content. base now clips and an inner [part='content'] around the default slot scrolls, which pins the close button structurally. Named header/footer slots ride outside the scroll: an unfilled slot is display: contents and costs the layout nothing, and a filled one makes the slotted node a flex row of base itself, sharing its gap and padding. The model wizard and hint editor slot their titles and action rows accordingly, and the pattern is documented in the ai-context fragment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"ImageBlock — block" said the name twice, every group's suffix was shaped differently, and "— another app" asserted an origin nothing established — getForeignShacl returns every SHACL shape in the dataset that is not WE's own, whoever put it there (checked: neither the executor nor WE auto-installs anything that would justify the claim). All three are one problem: groups being encoded as per-item string suffixes. So we-select learns what the list actually is. SelectOption gains `icon` and `group`: consecutive options sharing a group render under one non-interactive heading (role=presentation, skipped by keyboard navigation, dropped when its options are all filtered out), icons draw beside labels in the list and on the chosen value, and the fit sizer measures icon and gap so fitted widths stay honest. Truncation moves to a label span, so an icon is never what gets clipped. The relationship targets then group as "This space" (each shape wearing the icon its author picked in this wizard), "Blocks" (wearing the block composer's own iconography, so a block is drawn the same way wherever it is named), and "Other models in this space" — which claims only where the model lives, the one thing that is actually known. Also "Points at…" → "Links to…", which is friendlier and more literally true at once: a perspective is made of links. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion icons Three small fixes from testing. The modal's new scroll region clipped focus rings: a field stretches to the scroller's full width and its ring paints just outside itself, which the overflow clipping killed dead — measured at exactly 0px of room on every side. The scroller now carries 4px of padding taken back by an equal negative margin, so nothing moves and the ring has room to paint (the widest ring is 2px). The wizard wrapped its member rows: moving the width from the inner Column onto the modal made 720px include the modal's own padding — space-900 each side, so the rows lost 128px. The wizard and hint editor widths now account for it. Option icons in we-select are 16px and tinted primary-700, in the list and on the chosen value alike — they are wayfinding, not content, and read better as a system of accent markers than a column of dark glyphs. we-icon defaults its fill to currentColor, so one stylesheet rule covers every consumer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t generate themselves Four pieces from testing and design discussion. **The time picker's popup is now ours.** The calendar half of we-date-picker was custom precisely because native popups cannot be themed; the time half stopping at showPicker() was inconsistent with the component's own reasoning. The clock button now opens a floating list — half-hour steps, labelled for the locale (2:30 PM or 14:30, storing HH:mm either way), dressed exactly as we-select's listbox, opened pre-scrolled to the value or its nearest half hour. A combobox rather than wheels or a clock face, because time-picking is two tasks: common times want recognition, exact times want typing — and the field beside the list already types. It stays a native time input on purpose: touch devices keep the OS wheel, which no web dropdown improves on (and which color-scheme now renders dark correctly — that fix stays, it covers scrollbars and every other UA surface, not just the popup this replaces). **Drag-down showed its drop one row late.** _indexAt speaks the dragged-inclusive index space and the drop path converts before committing — but the indicator drew from the unconverted index against a dragged-exclusive list. The two spaces agree when dragging up and differ by one when dragging down, so the line sat a row below where the drop would land, and a downward drag had to be taken a row too far before the line reached the place already meant. **Fields can generate themselves.** An "Auto-generate fields" button on the Fields row runs the same generation as the describe-it flow, prompted by what the author already wrote — name, description, AI hint. Their words survive; the generation contributes the structure, plus answers for anything left blank. Enabled only when there is context to work from and no member row has been started, since generation replaces the member list wholesale and quietly discarding typed rows is how a click becomes a loss. New models only, same as the describe-it box. **A new draft opens with no members.** The pre-added blank row asserted the shape of the model before its author said anything — and put a property first when the right first move may be a relationship or the generate button. An empty state stands in its place; saving with nothing still fails with "a model needs at least one property". The member name inputs also now say which kind of row they name — they carry no label, so the placeholder is doing label duty, and the identifier validation already teaches the camelCase spelling at the moment it matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Model generation was gated on an Anthropic API key in agent settings — which only the template editor's AI panel ever sets — so on any profile without one the wizard's whole AI surface silently did not exist, including for the person who commissioned it. The expectation was always the AD4M-configured LLM: WE's other AI surfaces (transcription, extraction) already run on the node's models, and the key-gated path was the odd one out. The backend contract gains a language-model port — available() and one-shot prompt(system, input) — optional on the same terms as transcription. The AD4M adapter implements it over the executor's AI tasks: one task, found by name and reused (tasks persist across sessions, so creating one per call would accumulate), bound to the literal model id "default" so changing the default LLM in AI settings changes what answers, with no re-registration. Generation itself now runs on either transport: the backend port when the node has an LLM (preferred), the stored Anthropic key as fallback. The loop is one implementation over provider-neutral turns; the executor path carries the output schema in the system prompt — text is all the port speaks — and the parser forgives code fences and prose around the object, with the existing repair turns feeding validation errors back either way. And when neither is configured, the wizard now says so: a one-line hint stands in the describe-it box's place naming what would be there and pointing at Settings → AI, instead of the feature simply not existing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wizard had two ways to ask AI for a model: a "Describe it instead" box pinned above the form, and the generate-fields button on the Fields row. Two entry points to one outcome, and the box duplicated the very fields it sat on top of — you typed a description into it, an inch above a Description field. Since field generation already reads name, description and AI hint, and already fills whichever of them the author left blank, the button subsumed the box. So the box goes, its aiDescription state with it, and "Auto-generate fields" becomes "Generate fields" — the one AI route, fed by the form's own fields. Discoverability moves into the empty state, at the moment it is relevant rather than as a permanent panel: the generate route when a model is configured, and where to configure one when not. The richer-guidance story stays coherent without a prose box: description wants the one-liner every member will see, and the AI hint field is prompt payload by design — the legitimate home for what these records contain. shapeStore.generateShapeDraft (prose → whole draft) stays, unwired: it is the engine a spoken route needs — press record, transcription in, the same review form out — and deleting it would be rework the moment that lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Typing a name and description left "Generate fields" disabled; adding an empty property enabled it — exactly backwards, and both for one reason. setShapeField mutated the draft in place without publishing the signal, so the guard memo never re-ran while the context fields were typed, and first recomputed when a structural change (adding a row) happened to republish. In-place mutation is the member rows' concession — they live in an $each keyed by object identity, where replacement remounts the row and drops input focus. The top-level fields are ordinary controlled inputs outside any keyed loop; they now publish. The same staleness also worked in reverse, against the guard's whole purpose: member rows do mutate in place, so typing into one could leave the button enabled a keystroke long and a click would replace rows somebody just wrote. The action now recomputes the guard fresh at call time, and the member name and hint inputs publish on blur so the button's state follows a touched row as soon as it is left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion shrink sideways Two layout faults with the same shape — a box refusing to be the size its container offers — found while working through the model wizard, and both general enough to fix where they live rather than in the template. `we-modal` now sets `scrollbar-gutter: stable` on its scroll region. Crossing the scroll threshold was also a relayout: the bar took its width out of the content box, so expanding one section of a form narrowed every control in the modal. Nearly free here, since the gutter is the 6px of the scrollbar token rather than a native bar's width, against a transparent track. `ConditionalRenderer` relaxes the reveal wrapper's automatic minimum on both axes, not just the one being revealed. A grid item's automatic minimum size is its content on either axis, so a block-revealing section kept `min-width: auto` — and a line of `white-space: nowrap` text has a min-content width of the whole line. Such a section refused to shrink, pushed out of its container, and took its own `text-overflow: ellipsis` out of reach on the way. The wrapper exists only to animate; it has no business imposing a minimum on either axis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uthor's own words Five rounds of manual testing on the model wizard, and the thread running through them is the same: what the machine wrote and what the author wrote were never distinguished, so a re-run could neither be offered safely nor produce a coherent answer. **Generation re-runs.** `canAutoGenerateFields` latched off the moment any row was touched — which every generated row is — so the first attempt was the only attempt, and spending it on a bare name was a trap. It is replaced by `generateIntent`: 'generate' (nothing to lose), 'regenerate' (a proposal nobody has touched, so one click), 'replace' (rows somebody wrote, so it asks first), 'none' (nothing to work from, the only disabled state). The store makes the ask/don't-ask decision because only it can tell the two apart, via a signature over the rows it last produced. **A re-run follows the rename.** The prompt was built from whatever the draft held, so a previous generation's own description and hint were quoted back as part of the next question — renaming a model to "MovieNight" while its generated description still discussed books returned a model about both. `authoredFields` draws the line: a field still holding exactly what generation put there is output, not intent, so it neither steers the next prompt nor survives it, while anything typed does both. Pressing Regenerate twice over an untouched draft is now a fresh attempt rather than a slow convergence on the first. **A generated `select` arrived with an empty default picker.** `defaultOptions` is derived, and every construction site spread a blank row and assigned over it, leaving the picker describing the blank until the allowed values were retyped. The new `draftMember` factory always ends in `syncDerived`, which makes that unrepresentable rather than something each caller must remember. Booleans had the same fault. **A hint written in the wizard came back blank.** The hint editor reached for the declaration only when *nothing at all* was stored — never true for a saved model — so every property opened empty while the class hint, whose fallback was written the other way, came through. `hintToDisplay` states one rule for both, using the customized marker to separate "never touched" from "deliberately cleared", which storage cannot distinguish on its own. **The wizard's shape**, from the same testing: generate moves beside the name (where a name is all it needs, and where it can no longer claim to make only "fields"); icon leads the row, since generation fills it; a collapsed property shows its hint, so generation no longer expands every row it produced; row buttons are pinned top-right like the drag handle rather than wrapping under the fields; Required becomes a checkbox; "Identifies duplicates" becomes "Identifying field", which names the role without promising a constraint nothing enforces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for coasys-we ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ES2020 target `declCSS`/`stateDeclCSS` reached for `String.replaceAll`, which is ES2021. The workspace's tsconfig targets ES2020, so `lib` is ES2020 and the method exists nowhere the type checker looks — `@we/design-utils`' own `tsc --noEmit` failed on both call sites. It went unnoticed because nothing ran the whole pipeline: tsup transpiles without checking against the package's `lib`, so every build passed, and CI never reached its Typecheck step because the AD4M source build ahead of it was failing for unrelated reasons. A shared global regex rather than a bump to the workspace target: retargeting every package is a repo-wide decision, and this is two call sites. `String .replace` resets a global pattern's `lastIndex` itself, so one shared regex is safe across calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The build cloned the AD4M repo, compiled its SDK from source, and pointed `pnpm.overrides` at the result — so it judged every WE change against whatever AD4M's `dev` branch happened to hold at the moment the job ran, having first verified and then discarded an install of the versions this repo actually pins. Three consequences, all of them observed. The same WE commit could go from green to red with no WE change: `dev` sat red for two days while every developer's machine built cleanly, because an AD4M decorator option WE depends on lives in a published tag and on a feature branch, but not on AD4M `dev`. A green build said nothing about the combination anyone runs or would ship. And the failure arrived as a TypeScript error inside WE's own source, naming nothing that would lead a reader to another repository. The premise for it does not hold either. Published `@coasys/ad4m` carries no `workspace:` refs at all, and `@coasys/ad4m-connect`'s single one sits in `devDependencies`, which a consumer never installs — which is why the pinned install this job already performed, and every developer's machine, work fine. So the required build installs the lockfile and stops there. Testing WE against unreleased AD4M keeps its value and moves to `ad4m-compat.yaml`, which runs nightly against AD4M `dev` and on demand against any branch — the cross-repo path, still available, no longer able to decide whether a WE change may merge. Two things fall out. The install can be frozen, since nothing rewrites the manifest or the lockfile mid-run. And the generated-files check drops all three of its exclusions — `package.json`, `pnpm-lock.yaml` and `ad4m/` were things this workflow put there and then agreed not to notice, so the check now covers the whole tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`shows no time in the field until the value has one` matched `/14:30/`, which is not what the picker promises. It formats through `toLocale*String(undefined, …)` deliberately — a 12-hour reader should read "2:30 PM" while "14:30" is what gets stored — so the assertion was pinned to the machine it was written on. It passed here and failed on a CI runner formatting US English. Two sibling assertions had already met this and hedged with `/2:30|14:30/`: the same defect, noticed and worked around rather than fixed, and weaker for it — an alternation like that also passes on a picker rendering the wrong one. All three now derive their expectation from the same `Intl` call the component makes, so they stay exact about the minute while saying nothing about which locale is running. `el.value` assertions stay literal, since the stored value is 24-hour by contract. Verified under en-GB, en-US, de-DE and ja-JP, and the whole workspace suite under en-US in both UTC and America/Los_Angeles — 2,253 tests, no failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Model authoring: spaces define their own content models
Branch:
feat/model-authoring(fromdev) — 49 commits, 75 files.Summary
Communities can now create and edit content models in a space — through a structured wizard, or by
naming one and having the space's language model fill in the rest — and tune the interpretation
hints AI extraction runs on, per space, for core vocabulary (TaskBlock, EventBlock) and their own
models alike.
The architecture is one IR, two UIs, one adoption gate. Both authoring routes produce the
same draft, lowered onto the same
ModelManifest, validated by the same gate module manifests pass,stored in the space as a
Shaperecord (the Template storage precedent: one flag-typed entity plusthe definition JSON in a file-storage property), compiled per-dataset, and projected to SHACL — after
which
$query, includes and extraction see the entity through machinery that already existed (theforeign-schema rail). The manifest document is the source of truth; the SHACL is its staleness-checked
projection, exactly the relationship decorated classes have with theirs. This is stage-0
infrastructure for the content-models plan: created models are the flat-record corner of the shape
continuum, and stage A extends this IR rather than replacing it.
The wizard went through five rounds of manual testing, and most of the second half of this branch is
what that found. The thread running through it: what the machine wrote and what the author wrote
were never distinguished, so a generation could neither be re-run safely nor produce a coherent
second answer. That distinction is now explicit and is what makes the AI route usable rather than
one-shot.
Changes
The IR
Manifest fidelity (
backend-shared/manifest.ts,manifestEntry.ts,ad4m/manifestCompiler.ts,neutralManifest.ts,perspectiveHelpers.ts,models/scripts/generateCoreManifest.mjs+regenerated
coreManifest.ts, golden tests)PropertySchemagainsinterpretationHint,identity,options;EntitySchemagainsinterpretationHint. The declared-manifest path was the one path that silently dropped hints — itcan now carry everything the executor reads off a stored shape.
golden test compares hints and identity, so a future miss fails loudly instead of surfacing as "the
model extracted nothing".
validateManifest: at-most-one-identity rule,optionstype/default checks,externalEntities(targets may name core vocabulary or sibling shapes), and a fix for
abstractbeing silentlystripped by the Zod schema.
Validation at registration (
moduleRegistry.ts) —definition.entities.manifestpassesvalidateManifestbefore registration, instead of failing on the first dataset switch, far from themodule that shipped it.
Space-owned interpretation
Hints (
ad4m/interpretationHints.ts,sdnaModels.ts,backendPorts.ts, both adapters)SchemaPortsurface:interpretationHints/setInterpretationHints/resetInterpretationHints, keyed by predicate.we://interpretation_customizedmarker on the shape node makes hints space-owned:shapeIsStaleyields on hints for marked shapes (structure — paths, identity — stays code-owned and compared as
before), so a community's tuning survives space switches. Reset rewrites the declared hints and
clears the marker, so release improvements flow again.
customizations.
Reading hints back (
shared/shapes/hintEditor.ts+ test) —hintToDisplaystates one rule forthe class hint and every property hint, which previously disagreed. An empty hint is stored by
removing its link, so "cleared on purpose" and "never touched" read back identically; the customized
marker is the tiebreak. Not customized → an absent hint means the declaration applies. Customized →
the space owns these, so an absent hint is one somebody deleted and must not be resurrected. The old
property branch reached for the declaration only when nothing at all was stored — never true for a
saved model — so every property opened blank while the class hint came through correctly.
The Shape entity and its adoption rail
models/entities/Shape.ts,modelRegistry.ts,backendPortsAdapter.ts,app-shell ShapeStore.tsx,shared/shapes/shapeDraft.tsShape: name, description, icon,shapeId(stable identity URI — the ecosystem convergencemechanism),
forkedFromlineage,version,definition(manifest JSON). Installs with the spaceSDNA set; predicates and the type flag are resolved before storing under
we://shape/<uuid>/…, sodefinitions are self-describing and a rename can never re-mint the storage keys existing data lives
under.
SchemaPort.declareInDataset+mergeDynamicModels: compiled shape classes register in their owndataset only (native classes still shadow everything), additive alongside the foreign-schema sweep's
replace semantics.
ShapeStoreowns the rail — load → validate → compile → register → ensure SHACL, per space switch;invalid definitions surface as per-shape
problemsrather than vanishing.shared/shapes/shapeDraft.ts): identifier/type/optionvalidation with wizard-facing messages, predicate preservation through edit round-trips, and the
v1 additive-only edit guard.
additiveViolationscompares what each surviving predicatemeans, not merely that it survives: a
count: numberedited tocount: textkeeps the storage keyand silently changes how every stored value reads. Kind changes, scalar type changes, relation
re-targets and many→one are refused; one→many is the single widening that needs no migration.
draftMemberis now the one supported way to build a row, and always ends insyncDerived.Spreading a blank row and assigning over it — what every construction site did — left
defaultOptionsdescribing the blank, so a generated
selectarrived carrying its allowed values beside a defaultpicker offering only "None", and only retyping those values ever put the two in step. Booleans had
the same fault.
The wizard
templates/default … SettingsRoute/ModelsSection.ts,ShapeStore.tsx, ai-context store entriesA Models card in space settings: the space's models with problems visible, the wizard, per-space
AI-hint tuning with the customized badge and reset lifecycle, and delete with confirm (data and SDNA
remain; only the definition goes).
property(type/required/hint/options/default) or
relationship(target + one-or-many). It was one row with areferencepseudo-typethat hid half its own inputs — a tagged union pretending not to be one, when
EntitySchemahascarried
propertiesandrelationsas separate maps all along.it, so N independent switches could express a violation the save then had to refuse. Keyed by a
draft-local row id, so renaming or reordering the chosen field keeps the choice. Labelled
"Identifying field" — "Unique field" would promise a constraint nothing enforces.
of the derived creation form. This needed two fixes in
we-sortablefirst, both no-ops for an itemwithout form controls: an optional
[data-we-handle]grab area, and a rule that Space or Enterarriving from a text-entry element is typing rather than a pickup — without which a hint field could
not accept spaces at all.
a narrow modal no longer folds them under the fields where they read as belonging to whichever field
they landed beside. Required is a checkbox rather than a switch: a switch says "this takes effect
now", which is a setting, and its label changed width as it toggled.
expanding every row it produced. The condition leads with the reactive operand deliberately —
$andshort-circuits, and leading with the non-reactive$member.hintleft rows that mount withoutone with no reactive dependency at all, so the line could never appear however much was typed.
core infrastructure (
Template,Theme,AgentSettings) no longer offered.AI authoring
shared/ai/shapeGeneration.ts,backend-shared/languageModel.ts,ad4m/languageModelPort.ts,ShapeStore.tsxOne forced tool call whose schema mirrors the draft — the closed type set is unrepresentable to get
wrong — validated through the same gates as a hand-built draft, with up to two tool-result repair
turns. The result lands in the wizard with any remaining problems displayed: generation proposes, the
human saves.
BackendPorts.languageModel, with the agent'spersonal Anthropic key as fallback. Gating on the key alone hid the whole AI surface from anyone who
had not used the template editor, however capable their node was.
generated row is — so the first attempt was the only attempt, and spending it on a bare name was a
trap.
generateIntentreplaces it:generate(nothing to lose),regenerate(a proposal nobodyhas touched, so one click and no dialog),
replace(rows somebody wrote, so it asks first),none(nothing to work from — the only disabled state). The store decides, because only it can tell a
proposal from somebody's own work.
generation's own description and hint were quoted back as part of the next question: renaming a
model to "MovieNight" while its generated description still discussed books returned a model about
both.
authoredFieldsdraws the line — a field still holding exactly what generation put there isoutput, not intent, so it neither steers the next prompt nor survives it, while anything typed does
both. A side effect worth having: pressing Regenerate twice on an untouched draft is now a fresh
attempt rather than a slow convergence on the first.
produce only "fields" — it answers the description, hint and icon too, for anything left blank.
Design system
Fixes found by building the wizard, all general enough to belong where they live:
we-modal:scrollbar-gutter: stable, so crossing the scroll threshold is not also a relayout —expanding one section used to take the bar's width out of the content box and narrow every control.
Modals scroll their content only, with header and footer slotted out of the scroll region.
ConditionalRenderer: a reveal wrapper relaxes its automatic minimum on both axes. A grid item'sautomatic minimum is its content on either axis, so a block-revealing section kept
min-width: auto— and nowrap text has a min-content width of the whole line, which pushed such a section out of its
container and took its own ellipsis out of reach.
we-selectgains an opt-infitsized to its widest option, and keeps that width on hover;we-number-inputsizes from its content, measured rather than guessed;we-date-pickerkeeps itsicon inside the control and themes its time row;
we-form-fieldgains the type rampwe-textalready defines, with the description above the control;
we-icon-pickerscales its text with itssize; anchored panels float in the top layer through one shared
floating-panelhelper; dark-schemenative widgets;
overflowreaches[part='base']; z-index tokens.CI
One commit here is not about model authoring — the workflow change — and would land faster as its own
PR off
dev, sincedevis red without it and nothing on any branch can go green until it lands.The two fixes below it are this branch's own defects, so they belong here whatever happens to the
workflow change.
The required build now installs the versions this repo pins. It used to clone AD4M, build its SDK
from source and point
pnpm.overridesat the result — judging every WE change against whatever AD4Mdevheld at the moment the job ran, after verifying and then discarding an install of the pinnedversions. So the same WE commit could go from green to red with no WE change, and did:
devhas beenred since 18 Aug, when WE took a dedup fix that exists in a published tag and on an AD4M feature
branch but not on AD4M
dev. The stated justification — brokenworkspace:refs in the publishedpackages — does not hold:
@coasys/ad4mpublishes none at all, and@coasys/ad4m-connect's singleone sits in
devDependencies, which a consumer never installs.Testing WE against unreleased AD4M keeps its value and moves to
ad4m-compat.yaml— nightly againstAD4M
dev, on demand against any branch — where it can warn without gating, and where a cross-repochange is requested explicitly rather than inferred from two repositories sharing a branch name. Two
things fall out: the install can be frozen, and the generated-files check drops all three exclusions,
which existed only to hide the workflow's own rewrites.
It immediately exposed two defects of this branch's own, both of which had been sitting behind a
failure that had nothing to do with them — which is the argument for the change in miniature:
@we/design-utilsreached forString.replaceAll(ES2021) while the workspace targets ES2020, sothat package's
tsc --noEmitfailed. tsup transpiles without checking against a package'slib,so every build passed, and CI never reached its Typecheck step. Fixed with a shared global regex
rather than retargeting the workspace, which is not this PR's call to make.
toLocale*String(undefined, …)deliberately — a 12-hour reader should read "2:30 PM" while"14:30" is what gets stored — so
toMatch(/14:30/)asserted the machine it was written on andfailed on a runner formatting US English. Two siblings had already met this and hedged with
/2:30|14:30/, which is weaker still: that alternation passes even when the picker renders thewrong one. All three now derive their expectation from the same
Intlcall the component makes,so they are stricter than before (
toBeagainst a computed reference) and say nothing aboutlocale.
Known follow-ups
note (content-models plan stage F); v1 is additive-only by guard.
extends WeNodeyet (cross-manifest extends isn't compilable), so comments andsignals don't attach to shape instances.
interpretationHints.ts); needs themerge story from the versioning note.
extractable now, but templates render them by hand.
generateShapeDraft(prose → whole draft) has no control wired to it; it stays as the engine aspoken route would need. Decide whether to keep or drop it before merge.
one with lineage, are the structural counterparts — see the content-models plan.
styles, because something in the running app relaxesit on hover and the rule was not found by reading (searched: every
:hoverin the design system, theinterop stylesheet,
we-sortable, the themes). Worth another look if the same symptom appearselsewhere.
we-previewharness was not exercised.Test plan
pnpm lint,pnpm lint:css,pnpm build,pnpm validate:schemas,pnpm typecheck,pnpm test: all pass, against thepinned dependency versions. This is the point of the CI change: the pipeline a developer can
run is the pipeline that gates the merge
pnpm buildleaves the tree clean with no exclusions, so the strengthened generated-filescheck passes
LANG=en-US TZ=UTC, and again underAmerica/Los_Angeles, since a western timezone shifts dates as well as formats. 2,253 tests,no failures, so nothing else in the workspace depends on the machine it runs on. (Confirmed
first that
LANGdoes move Node's ICU locale here, so the runs meant something:en-USreproduces CI's "2:30 PM" exactly)
we-validate-schemas— 28 schemas, no issuesfirst-contact bugs, the default-value controls, the property/relationship split, the AI route,
and the layout/animation pass
draftMemberfactory deriving pickersat construction (including a generated
selectandboolean);authoredFieldsandmemberSignature;hintToDisplay's four cases; staleness yield rules; golden manifest testscomparing hints and identity;
we-sortable's handle and text-entry rules; the reveal wrapper'sminimum on both axes
$query; edit additively;edit TaskBlock hints, run extraction, switch spaces, confirm hints survive; reset hints
defaults" or "Customized for this space". The fallback fix stops masking the difference: if the
hints show but the badge says defaults, they may not be reaching the SHACL — and since the
executor builds extraction prompts from the shapes stored in the perspective, that would make
them inert for extraction
arrows, Space); confirm typing spaces in a hint field still types spaces