@akanjs/cli 3.0.0-alpha.37 → 3.0.0-alpha.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.build-stamp CHANGED
@@ -1 +1 @@
1
- 0f49fb72ef07c2b78899d477bb99067563e42d917439e87893099da3a0fbc111
1
+ 30b6841e9753c3daa11bfb9f58d5e50f030a8a51a63c2c1070e02a7f041cbf68
@@ -73,6 +73,14 @@ that looks wrong; do not "fix" it back.
73
73
  of a component nested inside another one. A function-typed prop (`onPick?: (t: cnst.LightTicket) => void`) is
74
74
  exempt for the same reason — a closure cannot cross the RSC boundary at all, so whoever passes it is a client
75
75
  component already holding the value.
76
+ - **Never wrap a form setter in a pass-through arrow** (`no-unpublished-form-setter.grit`).
77
+ `onChange={(type) => st.do.setTypeOnTicket(type)}` runs identically to `onChange={st.do.setTypeOnTicket}` —
78
+ generated field setters take exactly one value — but the arrow is a fresh anonymous closure, so the control
79
+ emits no `data-akan-action` and publishes no agent tool for the field. The failure is silent and the two lines
80
+ read the same, which is why it is a lint error. A wrapper that transforms the value
81
+ (`st.do.setNameOnX(toCamelCase(name))`), adds a statement, or writes a nested path with `writeOnX` is doing
82
+ something a reference cannot and stays legal — publish that one with an explicit `st.tool`. Scoped to
83
+ `{apps,libs}/**/*.tsx`; a typed parameter is not matched, so it under-reports rather than misfiring.
76
84
  - **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
77
85
  `../map/map.constant` are the sanctioned exception.
78
86
  - **Never import across the client/server boundary.** Client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every
@@ -681,6 +689,19 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
681
689
  providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, ClaudeLlm)`, where the
682
690
  implementation is an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)` — ignore
683
691
  `onDelta` and the chat still answers whole.
692
+ - **A file the user attaches rides the message, and nothing is stored.** The composer takes a paperclip, a drop and
693
+ a paste; an image rides as bytes and a text file as text, which is all a browser reads with no dependency.
694
+ Everything else is the app's own reader — `<Agent.Chat attach={…} />`, one `File` in, a `MessageAttachment` or
695
+ `null` out — because extracting a PDF needs a parser and the framework carries attachments without depending on
696
+ one. It runs ahead of the built-in, so it is also where an image is downscaled before it costs a megabyte of
697
+ prompt. **What the provider cannot read is replaced by a note naming the file**, never dropped: an attachment the
698
+ model never saw is one it answers about from the filename. An adaptor declares `accepts: { image, document }` and
699
+ `AgentService.readable` degrades the rest, so a text-only provider needs no attachment code at all — DeepSeek
700
+ declares none, which is why an image against the default provider is refused out loud while an extracted PDF
701
+ works, `text` being readable by every model there is. **A `prompt()`'s `Msg.image` is the same wire shape** and
702
+ reaches the chat as an attachment rather than the literal `[image]` it used to become. Persisting keeps each
703
+ attachment's name and drops its content: web storage is a few megabytes, one screenshot fills a chunk of it, and
704
+ a save that fails is silent — so keeping the bytes would quietly stop keeping the transcript.
684
705
  - **A dialog's close is the dialog's own dismissal, not a state flip.** `closeDialogIn<Ns>` (and `Dialog.Close`)
685
706
  run through whatever `Dialog.Modal` registered, so the agent takes the exact path the X button takes —
686
707
  `confirmClose` still prompts and `onCancel` still fires. A close that only set `open` to false would skip both,
@@ -735,16 +756,25 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
735
756
  `new<Model>`; `Load.Units`, `Load.Pagination` and `Data.Pagination` publish `setPageOf<Model>`; `Layout.Sider`,
736
757
  `System.SelectLanguage`, `Link.Back` and `System.ThemeToggle` publish the shell. **A component that can render
737
758
  twice on one screen takes a `namespace` prop and publishes nothing without it** — `Tab`, `Dialog`,
738
- `ScreenNavigator`. Pass one (`<Tab namespace="detail">`) and the tool becomes `switchTabInDetail`; leave it off
739
- and that tab is invisible to the agent, because two tabs answering to `switchTab` would mean the first to mount
740
- loses. `Model.NewWrapper` takes the same prop but publishes without one, because its slice already names it —
741
- a second create trigger for the same slice, opening a form seeded differently, is what needs the suffix.
759
+ `ScreenNavigator`, `Dropdown`. Pass one (`<Tab namespace="detail">`) and the tool becomes `switchTabInDetail`;
760
+ leave it off and that tab is invisible to the agent, because two tabs answering to `switchTab` would mean the
761
+ first to mount loses. A named `Dropdown` publishes `openDropdownIn<Ns>` / `closeDropdownIn<Ns>` and the state
762
+ `dropdownIn<Ns>`, and its trigger annotates whichever of the two its next click performs. `Model.NewWrapper`
763
+ takes the same prop but publishes without one, because its slice already names it — a second create trigger for
764
+ the same slice, opening a form seeded differently, is what needs the suffix.
742
765
  - **The `Model.*` row wrappers publish their verb, taking the id.** `Model.EditWrapper`, `Model.ViewWrapper`,
743
766
  `Model.RemoveWrapper` and `Model.Remove` publish `edit<Model>` / `view<Model>` / `remove<Model>` with a
744
767
  `modelId` argument, so a list built from `Load.Units` and an app's own `Unit` reaches the same verbs an
745
768
  `AdminPanel` does. `Model.SureToRemove` publishes the same — except under `typeNameToRemove`, where it
746
769
  publishes nothing: that gate makes a person retype the model's name, an approval card is one click, and
747
770
  offering the lever at a friction the screen does not have is not the same control.
771
+ - **A dropdown's menu is mounted from the first render and hidden while closed** — the deliberate opposite of the
772
+ modal rule below, because a menu is one click away rather than a surface of its own. A tool is declared by a
773
+ mount effect, so an unmounted menu is one whose row verbs and field setters do not exist yet: an agent asked for
774
+ one finds nothing, and no catalogue entry hints that opening the menu would help. `readScreen` still skips
775
+ hidden content, so the items themselves are read only after `openDropdownIn<Ns>` — what a closed menu publishes
776
+ is its tools, not its text. The cost is that `content` renders on page load, so a heavy panel belongs behind a
777
+ `Dialog` instead.
748
778
  - **A modal publishes its verbs while it is open, and only then.** `Model.EditModal` publishes `submit<Model>`
749
779
  and `cancelEditOf<Model>`, `Model.ViewModal` publishes `closeViewOf<Model>`, and `Model.ViewEditModal`
750
780
  publishes `edit<Model>` / `submit<Model>` / `closeViewOf<Model>` — each from a subtree that mounts with the
@@ -764,6 +794,79 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
764
794
  relation (picked or uploaded, not typed), a base document field, or a `hidden`/`secret` one at any depth —
765
795
  their reads are masked and a writer would be the door around that. `st.use.taskForm({ agent: false })`
766
796
  withholds the patch tool; an inline arrow withholds a control's own.
797
+ **The patch writes each plain field through that control's own published tool, not the setter underneath it** —
798
+ which is what carries the control's `transform`, so a field cannot normalize one way for `setPhoneOnBizAccount`
799
+ and another way for `fillBizAccountForm`. Only a composite, having no control, dispatches its setter directly.
800
+ The patch tool is registered `shared`, because the entry is a pure function of the model: a form put on screen by
801
+ a shell that subscribes it (`Model.EditModal`) *and* by the `Template` inside it is one declaration twice over,
802
+ not a clash — so neither has to suppress the other, and neither is asked to.
803
+ - **A `disabled` control publishes nothing, so the agent never gets a lever the person cannot pull.** Every value
804
+ control reads it — `Field.*`, `Input.*`, `Select`, `Switch`, and the four relation pickers — and disabling a
805
+ mounted control withdraws its tool for as long as it stays disabled. One gate covers both writers: with no
806
+ control published, `fill<Model>Form`'s guard refuses that field too. `readScreen` says `(disabled)` beside the
807
+ control, from the native attribute or `aria-disabled`, so a refusal is something the agent could have read first
808
+ rather than a surprise. This is the same rule as publishing only where the screen renders the control, applied to
809
+ a control the screen renders but withholds.
810
+ - **Whatever the wrapper was for, there is a place to put it that is not the wrapper.** An inline arrow is the one
811
+ shape that publishes nothing, so each reason for writing one has its own home, and reaching for that home is what
812
+ keeps the field reachable:
813
+ - *normalize* — `(v) => set(formatPhone(v))` becomes the control's own `transform` prop, which every text and
814
+ number `Field.*` already takes (`Field.Phone` defaults it to `formatPhone`). `onChange` stays a reference, and
815
+ **`transform` runs on the agent's write too**, by both paths — the field's own tool and `fill<Model>Form`,
816
+ which goes through the control to get it — otherwise a person would store `010-1234-5678` and an agent the raw
817
+ digits. It normalizes one scalar, so an array control applies it per element and a cleared nullable field stays
818
+ null. It is the *control's* rule, though: a rule that must hold however the field is written — including a
819
+ composite path or a base-document write — belongs in `_postSet<Field>` below.
820
+ - *multi-write* — `(v) => { set(v); other(v); }` becomes a **`_postSet<Field>` method on the store**, and the
821
+ control keeps handing over the generated setter by reference. It runs right after the field is written, so it
822
+ reads the new value, and it reaches every other generated action with `this.` —
823
+ `_postSetToBiz(toBiz) { if (toBiz) this.addSendEmailsOnEstSheet(toBiz.sendEmails ?? []); }`. Nothing about the
824
+ control changes, so `data-akan-action` **and** `data-akan-state` both survive, and the rule now fires for every
825
+ writer — the person, the agent, `fill<Model>Form` — which is what a rule about a field should do.
826
+ **A generated action cannot be overridden, so do not try.** They all come from mapped types, and a mapped type
827
+ produces *properties*: a subclass method of the same name is `TS2425`, optional or not, and the two shapes
828
+ TypeScript does allow — a class field and a getter — are both skipped by `StoreRegistry.register`, which only
829
+ collects prototype descriptors holding a function. There is no legal middle, which is exactly why the hook
830
+ carries a leading `_` and no model suffix: a name no mapped type can produce is the only name a subclass may
831
+ declare. It cannot be typed either, for the same reason, so a misspelled field is named on the console at
832
+ registration instead. Calling a generated action *from* a custom one is fine and always was — `this.setXOnY(v)`
833
+ typechecks anywhere.
834
+ - *nested path* — `(v) => writeOnTask("payments.3.name", v)` has no home and needs none: an embedded row is
835
+ unannotatable by design, and an agent reaches it through `fillTaskForm`, which waves composites through.
836
+
837
+ `no-unpublished-form-setter.grit` errors on the pure-forwarding shape only, because every other one has a
838
+ legitimate reading. **`akan quality scan` counts them all** (`akan.agent.unpublished-form-setter`, one warning per
839
+ file): the lint rule is the per-line enforcement, the scan is the inventory of fields this screen writes but
840
+ cannot be asked to write.
841
+ - **A relation reaches an agent from its picker, not from the form patch.** `fillTaskForm` publishes no schema for
842
+ one and is right not to: the form holds the whole related document, so an id would need a lookup the store does
843
+ not do. The picker is where that lookup lives, so `Field.Parent` / `Field.Children` publish the pair themselves —
844
+ `load<Field>OptionsOn<Model>`, which loads the slice and returns `[{ id, label }]`, and the field's own
845
+ `set<Field>On<Model>` taking `<field>Id` / `<field>Ids`. Listing is its own tool because loading is its own step
846
+ for a person too: the options arrive when the dropdown opens, and an agent never opens it. `Field.ParentId` /
847
+ `Field.ChildrenId` need none of that — the id *is* the value, so the ordinary setter describes it. All four still
848
+ require the setter **by reference**, and a `disabled` picker publishes nothing.
849
+ - **An array of embedded rows also publishes `add<Field>On<Model>` and `sub<Field>On<Model>`** — append, and
850
+ remove-by-position — beside the whole-array setter. Not new authority: the setter can already produce any array
851
+ those two can, so they are strictly weaker. What they add is that neither can touch a row it was not given, and
852
+ that is the point: writing the whole array means echoing every row the agent is *not* changing, `checked`
853
+ validates types and not values, so one mistyped row nobody asked about is written silently. Both take a list and
854
+ act atomically, because removing positions one call at a time would shift the ones not yet removed. **Only an
855
+ embedded-row array gets them** — an array of primitives or of relation ids has nothing to retype wrong, its
856
+ values *are* the payload, so it keeps one setter and pays for no extra tools. `add` appends and publishes no
857
+ insert position, matching the `+` a person presses; `addOrSub` is never published, since it matches by `indexOf`
858
+ and would compare rows by reference. Editing a row in place stays `fill<Model>Form`'s job.
859
+ - **A list the person can drag also publishes `move<Field>On<Model>(from, to)`**, and `DraggableList` is a form
860
+ control like any other: handed the generated setter by reference it publishes that field, so an app that renders
861
+ its own rows with `DraggableList` writes no `st.tool`. The reorder tool exists for the same reason `add`/`sub` do
862
+ — the drag is the lever the screen offers and it changes no entry's content, so moving one row should not mean
863
+ retyping the nine beside it. No store action answers to it: reordering *is* a whole-array write, so the tool
864
+ splices the live entries and hands them to the setter the drag hands them to, `transform` deliberately not
865
+ applied, since the values are stored already and dragging normalizes nothing. It comes from the control saying
866
+ it sorts, not from the field, so a plain `Field.List` publishes no reorder and a scalar field never gets one.
867
+ **A component that composes `DraggableList` and already published the field hands the inner list a wrapper** —
868
+ the two would otherwise register one name twice, and the outer one is the one holding `transform`. That is what
869
+ `Field.TextList` does, and the only place an inline arrow is the right answer rather than a bug.
767
870
  - **Reading is per key, not per store.** `st.useState(name, initial, meta)` publishes local state (read-only
768
871
  unless `set:` names a type) and `st.expose(name, value)` a derived value. A subscribed store key is listed in
769
872
  the state context block by name and pulled with `readState(key)`, masked by the model that key declares — while
@@ -803,9 +906,10 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
803
906
  - The framework publishes five built-ins on every store surface: `navigate` (internal paths only, the same
804
907
  router `Link` rides), `goBack` (this session's history — global, because history is not a control a page owns and
805
908
  a page that draws no back link is not one you may not leave), `readScreen` (the rendered DOM as compact text —
806
- headings, links, control values; the chat's own UI is skipped via `data-agent-ui`, and a password value is never
807
- read), `readState(key)` (one masked store key), and `highlight(target)`. Declaring a hook tool under one of those
808
- names shadows the built-in, so reuse them only to mean that.
909
+ headings, links, control values, and `(disabled)` on a control or button that has it; the chat's own UI is
910
+ skipped via `data-agent-ui`, and a password value is never read), `readState(key)` (one masked store key), and
911
+ `highlight(target)`. Declaring a hook tool under one of those names shadows the built-in, so reuse them only to
912
+ mean that.
809
913
  - **A tool that changes the screen waits for the screen before it answers.** `router.push` returns while the RSC
810
914
  payload is still in flight and a store action that fires `void fetch.*` commits a tick later, so `navigate`
811
915
  awaits `ScreenSettle.wait()` — DOM quiescence, bounded, because the client router hands its promise to nobody —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "3.0.0-alpha.37",
3
+ "version": "3.0.0-alpha.39",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "3.0.0-alpha.37",
37
+ "akanjs": "3.0.0-alpha.39",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "dayjs": "^1.11.20",