@akanjs/cli 3.0.0-alpha.38 → 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
|
-
|
|
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`;
|
|
739
|
-
and that tab is invisible to the agent, because two tabs answering to `switchTab` would mean the
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
807
|
-
read), `readState(key)` (one masked store key), and
|
|
808
|
-
names shadows the built-in, so reuse them only to
|
|
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.
|
|
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
|
+
"akanjs": "3.0.0-alpha.39",
|
|
38
38
|
"chalk": "^5.6.2",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"dayjs": "^1.11.20",
|