@akanjs/cli 3.0.0-alpha.34 → 3.0.0-alpha.36
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
|
+
195b252da01339bc871684c59b5d01377d4145d281c8e2f13c92a91f9174f9e0
|
|
@@ -681,6 +681,10 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
|
|
|
681
681
|
providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, ClaudeLlm)`, where the
|
|
682
682
|
implementation is an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)` — ignore
|
|
683
683
|
`onDelta` and the chat still answers whole.
|
|
684
|
+
- **A dialog's close is the dialog's own dismissal, not a state flip.** `closeDialogIn<Ns>` (and `Dialog.Close`)
|
|
685
|
+
run through whatever `Dialog.Modal` registered, so the agent takes the exact path the X button takes —
|
|
686
|
+
`confirmClose` still prompts and `onCancel` still fires. A close that only set `open` to false would skip both,
|
|
687
|
+
which is a different action wearing the same name.
|
|
684
688
|
- **`<Agent.Zone id="comments">` runs a second agent over one section, in parallel with the root.** Everything
|
|
685
689
|
mounted inside — `st.use` subscriptions, hook tools, Guides — belongs to that zone's own conversation *and*
|
|
686
690
|
stays visible to the root agent: **zones are views, never walls**, so wrapping a section costs the root nothing.
|
|
@@ -718,17 +722,36 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
|
|
|
718
722
|
callable still drives the click a person makes. It does not throw: a tool schema is built during render, and an
|
|
719
723
|
agent-tooling mistake that aborted the render would cost the route its server rendering. `st.useState`'s `set`
|
|
720
724
|
degrades the same way, to read-only.
|
|
721
|
-
- **A component that renders once per row
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
`
|
|
725
|
+
- **A component that renders once per row never closes over its row's id — it takes the id as an argument.** A
|
|
726
|
+
tool that captured its own row would be fifty registrations of one name, forty-nine of them shadowed, and the
|
|
727
|
+
survivor would remove whichever row happened to mount last. Take the id instead — `removeTask(taskId)`, never
|
|
728
|
+
fifty `removeTask` — and every row's registration is then interchangeable, so a row component may publish after
|
|
729
|
+
all: say so with `shared: true` and the repeats are one declaration rather than a warned-about clash. The ids
|
|
730
|
+
come from the `<slice>.items` resource `Load.Units` and `Data.ListContainer` already expose. `shared` is a claim
|
|
731
|
+
about the *tool*, not a way to quiet a console: two rows whose tools would do different things (a different
|
|
732
|
+
`modal`, a different redirect) are not interchangeable, and that one wants a `namespace` or nothing at all.
|
|
725
733
|
- **`akanjs/ui` publishes its own controls, so an app writes nothing for them.** `Data.ListContainer` (and every
|
|
726
|
-
`Model.AdminPanel`) publishes its toolbar and its row
|
|
727
|
-
`Data.Pagination` publish `setPageOf<Model>`; `Layout.Sider`,
|
|
728
|
-
`System.ThemeToggle` publish the shell. **A component that can render
|
|
729
|
-
`namespace` prop and publishes nothing without it** — `Tab`, `Dialog`,
|
|
730
|
-
(`<Tab namespace="detail">`) and the tool becomes `switchTabInDetail`; leave it off
|
|
731
|
-
the agent, because two tabs answering to `switchTab` would mean the first to mount
|
|
734
|
+
`Model.AdminPanel`) publishes its toolbar and its row verbs; `Model.NewWrapper` (so `Model.New` too) publishes
|
|
735
|
+
`new<Model>`; `Load.Units`, `Load.Pagination` and `Data.Pagination` publish `setPageOf<Model>`; `Layout.Sider`,
|
|
736
|
+
`System.SelectLanguage`, `Link.Back` and `System.ThemeToggle` publish the shell. **A component that can render
|
|
737
|
+
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.
|
|
742
|
+
- **The `Model.*` row wrappers publish their verb, taking the id.** `Model.EditWrapper`, `Model.ViewWrapper`,
|
|
743
|
+
`Model.RemoveWrapper` and `Model.Remove` publish `edit<Model>` / `view<Model>` / `remove<Model>` with a
|
|
744
|
+
`modelId` argument, so a list built from `Load.Units` and an app's own `Unit` reaches the same verbs an
|
|
745
|
+
`AdminPanel` does. `Model.SureToRemove` publishes the same — except under `typeNameToRemove`, where it
|
|
746
|
+
publishes nothing: that gate makes a person retype the model's name, an approval card is one click, and
|
|
747
|
+
offering the lever at a friction the screen does not have is not the same control.
|
|
748
|
+
- **A modal publishes its verbs while it is open, and only then.** `Model.EditModal` publishes `submit<Model>`
|
|
749
|
+
and `cancelEditOf<Model>`, `Model.ViewModal` publishes `closeViewOf<Model>`, and `Model.ViewEditModal`
|
|
750
|
+
publishes `edit<Model>` / `submit<Model>` / `closeViewOf<Model>` — each from a subtree that mounts with the
|
|
751
|
+
open modal, never from the component that merely holds it. That is what makes a list legal: `Data.CardList`
|
|
752
|
+
renders one editor per row, and at most one of them is ever open, so one name is registered rather than fifty.
|
|
753
|
+
It also means the verb is absent from the catalogue while nothing is open, which is honest — and costs the
|
|
754
|
+
agent nothing, because the catalogue is re-read on the turn that follows the tool call that opened the form.
|
|
732
755
|
- **A form control publishes its own setter, and reading a form publishes one tool that fills several at once.**
|
|
733
756
|
Both are free: an app writes no `st.tool` for a form. A `Field.*` / `Input.*` / `Select` / `Switch` handed
|
|
734
757
|
`onChange={st.do.setTitleOnTask}` **by reference** publishes `setTitleOnTask` while it is on screen — the same
|
package/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var commandModules = {
|
|
|
31
31
|
guideline: async () => (await import("./guideline.command-s4hn2bsy.js")).GuidelineCommand,
|
|
32
32
|
scalar: async () => (await import("./scalar.command-rm6k3c65.js")).ScalarCommand,
|
|
33
33
|
primitive: async () => (await import("./primitive.command-bw0nes6x.js")).PrimitiveCommand,
|
|
34
|
-
quality: async () => (await import("./quality.command-
|
|
34
|
+
quality: async () => (await import("./quality.command-3kd7z1mb.js")).QualityCommand,
|
|
35
35
|
repair: async () => (await import("./repair.command-mzwtejpk.js")).RepairCommand,
|
|
36
36
|
workflow: async () => (await import("./workflow.command-g2cgcgxn.js")).WorkflowCommand
|
|
37
37
|
};
|
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.36",
|
|
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.36",
|
|
38
38
|
"chalk": "^5.6.2",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"dayjs": "^1.11.20",
|
|
@@ -23,10 +23,79 @@ import { readdir, readFile, stat } from "fs/promises";
|
|
|
23
23
|
import path from "path";
|
|
24
24
|
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
25
25
|
import ignore from "ignore";
|
|
26
|
-
import
|
|
26
|
+
import ts3 from "typescript";
|
|
27
27
|
|
|
28
|
-
// pkgs/@akanjs/devkit/
|
|
28
|
+
// pkgs/@akanjs/devkit/formSetterScanner.ts
|
|
29
29
|
import ts from "typescript";
|
|
30
|
+
|
|
31
|
+
class FormSetterScanner {
|
|
32
|
+
static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
|
|
33
|
+
scan(sourceFiles) {
|
|
34
|
+
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".tsx")).flatMap((f) => this.#scanFile(f));
|
|
35
|
+
}
|
|
36
|
+
#scanFile({ file, sourceFile }) {
|
|
37
|
+
const wrapped = [];
|
|
38
|
+
const visit = (node) => {
|
|
39
|
+
if (ts.isJsxAttribute(node)) {
|
|
40
|
+
const setter = FormSetterScanner.#wrappedSetterOf(node);
|
|
41
|
+
if (setter)
|
|
42
|
+
wrapped.push({ setter, line: FormSetterScanner.#lineOf(sourceFile, node) });
|
|
43
|
+
}
|
|
44
|
+
ts.forEachChild(node, visit);
|
|
45
|
+
};
|
|
46
|
+
visit(sourceFile);
|
|
47
|
+
if (!wrapped.length)
|
|
48
|
+
return [];
|
|
49
|
+
const setters = [...new Set(wrapped.map((entry) => entry.setter))].sort();
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
rule: "akan.agent.unpublished-form-setter",
|
|
53
|
+
scope: "agent",
|
|
54
|
+
severity: "warning",
|
|
55
|
+
message: setters.length === 1 ? `${setters[0]} is reached through a wrapper, so that field publishes no agent tool and carries no data-akan-action.` : `${setters.length} field setters are reached through a wrapper, so those fields publish no agent tool and carry no data-akan-action: ${setters.join(", ")}.`,
|
|
56
|
+
file,
|
|
57
|
+
line: wrapped[0]?.line,
|
|
58
|
+
locations: wrapped.map(({ line }) => ({ file, line }))
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
static #wrappedSetterOf(attribute) {
|
|
63
|
+
if (!ts.isIdentifier(attribute.name) || !/^on[A-Z]/.test(attribute.name.text))
|
|
64
|
+
return null;
|
|
65
|
+
const initializer = attribute.initializer;
|
|
66
|
+
if (!initializer || !ts.isJsxExpression(initializer) || !initializer.expression)
|
|
67
|
+
return null;
|
|
68
|
+
const handler = initializer.expression;
|
|
69
|
+
if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler))
|
|
70
|
+
return null;
|
|
71
|
+
if (!handler.parameters.length)
|
|
72
|
+
return null;
|
|
73
|
+
return FormSetterScanner.#setterCallIn(handler.body);
|
|
74
|
+
}
|
|
75
|
+
static #setterCallIn(node) {
|
|
76
|
+
let found = null;
|
|
77
|
+
const visit = (current) => {
|
|
78
|
+
if (found)
|
|
79
|
+
return;
|
|
80
|
+
if (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
81
|
+
const { expression, name } = current.expression;
|
|
82
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "st" && expression.name.text === "do" && FormSetterScanner.#fieldSetter.test(name.text)) {
|
|
83
|
+
found = name.text;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
ts.forEachChild(current, visit);
|
|
88
|
+
};
|
|
89
|
+
visit(node);
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
static #lineOf(sourceFile, node) {
|
|
93
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// pkgs/@akanjs/devkit/ssrScanner.ts
|
|
98
|
+
import ts2 from "typescript";
|
|
30
99
|
var STATIC_COMPONENT_MIN_MASS = 4;
|
|
31
100
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
32
101
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
@@ -144,31 +213,31 @@ class SsrScanner {
|
|
|
144
213
|
});
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
|
-
|
|
216
|
+
ts2.forEachChild(node, visit);
|
|
148
217
|
};
|
|
149
|
-
|
|
218
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
150
219
|
return warnings;
|
|
151
220
|
}
|
|
152
221
|
#isMountEffect(sourceFile, node) {
|
|
153
|
-
if (!
|
|
222
|
+
if (!ts2.isCallExpression(node))
|
|
154
223
|
return false;
|
|
155
224
|
const callee = node.expression.getText(sourceFile);
|
|
156
225
|
if (callee !== "useEffect" && callee !== "useLayoutEffect")
|
|
157
226
|
return false;
|
|
158
227
|
const deps = node.arguments[1];
|
|
159
|
-
return !!deps &&
|
|
228
|
+
return !!deps && ts2.isArrayLiteralExpression(deps) && deps.elements.length === 0;
|
|
160
229
|
}
|
|
161
230
|
#getLoadCalls(sourceFile, node) {
|
|
162
231
|
const calls = [];
|
|
163
232
|
const visit = (child) => {
|
|
164
|
-
if (
|
|
233
|
+
if (ts2.isCallExpression(child)) {
|
|
165
234
|
const callee = child.expression.getText(sourceFile);
|
|
166
235
|
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
167
236
|
calls.push({ callee, node: child });
|
|
168
237
|
}
|
|
169
|
-
|
|
238
|
+
ts2.forEachChild(child, visit);
|
|
170
239
|
};
|
|
171
|
-
|
|
240
|
+
ts2.forEachChild(node, visit);
|
|
172
241
|
return calls;
|
|
173
242
|
}
|
|
174
243
|
#getTemplateStateWarnings(sourceFile) {
|
|
@@ -176,7 +245,7 @@ class SsrScanner {
|
|
|
176
245
|
return [];
|
|
177
246
|
const warnings = [];
|
|
178
247
|
const visit = (node) => {
|
|
179
|
-
if (
|
|
248
|
+
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
|
|
180
249
|
warnings.push({
|
|
181
250
|
rule: "akan.ssr.template-client-state",
|
|
182
251
|
scope: "ssr",
|
|
@@ -186,9 +255,9 @@ class SsrScanner {
|
|
|
186
255
|
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
187
256
|
});
|
|
188
257
|
}
|
|
189
|
-
|
|
258
|
+
ts2.forEachChild(node, visit);
|
|
190
259
|
};
|
|
191
|
-
|
|
260
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
192
261
|
return warnings;
|
|
193
262
|
}
|
|
194
263
|
#measureBalance(sourceFiles) {
|
|
@@ -229,14 +298,14 @@ class SsrScanner {
|
|
|
229
298
|
return components;
|
|
230
299
|
}
|
|
231
300
|
#getComponentNodes(statement) {
|
|
232
|
-
if (
|
|
301
|
+
if (ts2.isFunctionDeclaration(statement) && statement.body)
|
|
233
302
|
return [{ name: statement.name?.text ?? "default", node: statement.body }];
|
|
234
|
-
if (!
|
|
303
|
+
if (!ts2.isVariableStatement(statement))
|
|
235
304
|
return [];
|
|
236
305
|
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
237
|
-
if (!
|
|
306
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer)
|
|
238
307
|
return [];
|
|
239
|
-
if (!
|
|
308
|
+
if (!ts2.isArrowFunction(declaration.initializer) && !ts2.isFunctionExpression(declaration.initializer))
|
|
240
309
|
return [];
|
|
241
310
|
return [{ name: declaration.name.text, node: declaration.initializer }];
|
|
242
311
|
});
|
|
@@ -244,7 +313,7 @@ class SsrScanner {
|
|
|
244
313
|
#getTouches(sourceFile, node) {
|
|
245
314
|
const touches = [];
|
|
246
315
|
const visit = (child) => {
|
|
247
|
-
if (
|
|
316
|
+
if (ts2.isCallExpression(child)) {
|
|
248
317
|
const callee = child.expression.getText(sourceFile);
|
|
249
318
|
const bareName = callee.split(".").pop() ?? callee;
|
|
250
319
|
if (callee === "createContext" || callee === "lazy")
|
|
@@ -252,60 +321,60 @@ class SsrScanner {
|
|
|
252
321
|
else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
|
|
253
322
|
touches.push(bareName);
|
|
254
323
|
}
|
|
255
|
-
if (
|
|
324
|
+
if (ts2.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
|
|
256
325
|
touches.push(child.name.getText(sourceFile));
|
|
257
|
-
if (
|
|
326
|
+
if (ts2.isPropertyAccessExpression(child)) {
|
|
258
327
|
const root = getAccessRoot(child);
|
|
259
328
|
if (root === "st")
|
|
260
329
|
touches.push("st");
|
|
261
330
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
262
331
|
touches.push(root);
|
|
263
332
|
}
|
|
264
|
-
|
|
333
|
+
ts2.forEachChild(child, visit);
|
|
265
334
|
};
|
|
266
|
-
|
|
335
|
+
ts2.forEachChild(node, visit);
|
|
267
336
|
return touches;
|
|
268
337
|
}
|
|
269
338
|
#getTagNames(sourceFile, node) {
|
|
270
339
|
const tags = new Set;
|
|
271
340
|
const visit = (child) => {
|
|
272
|
-
if (
|
|
341
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
273
342
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
274
|
-
|
|
343
|
+
ts2.forEachChild(child, visit);
|
|
275
344
|
};
|
|
276
|
-
|
|
345
|
+
ts2.forEachChild(node, visit);
|
|
277
346
|
return tags;
|
|
278
347
|
}
|
|
279
348
|
#getMass(node) {
|
|
280
349
|
let mass = 0;
|
|
281
350
|
const visit = (child) => {
|
|
282
|
-
if (
|
|
351
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
283
352
|
mass += 1;
|
|
284
|
-
|
|
353
|
+
ts2.forEachChild(child, visit);
|
|
285
354
|
};
|
|
286
|
-
|
|
355
|
+
ts2.forEachChild(node, visit);
|
|
287
356
|
return mass;
|
|
288
357
|
}
|
|
289
358
|
#hasUseClient(sourceFile) {
|
|
290
359
|
const first = sourceFile.statements[0];
|
|
291
|
-
if (!first || !
|
|
360
|
+
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
292
361
|
return false;
|
|
293
362
|
return first.expression.text === "use client";
|
|
294
363
|
}
|
|
295
364
|
#hasVendorImport(sourceFile) {
|
|
296
|
-
return sourceFile.statements.some((statement) =>
|
|
365
|
+
return sourceFile.statements.some((statement) => ts2.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
|
|
297
366
|
}
|
|
298
367
|
#getVendorNames(sourceFile) {
|
|
299
368
|
const names = new Set;
|
|
300
369
|
for (const statement of sourceFile.statements) {
|
|
301
|
-
if (!
|
|
370
|
+
if (!ts2.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
|
|
302
371
|
continue;
|
|
303
372
|
const clause = statement.importClause;
|
|
304
373
|
if (clause?.name)
|
|
305
374
|
names.add(clause.name.text);
|
|
306
|
-
if (clause?.namedBindings &&
|
|
375
|
+
if (clause?.namedBindings && ts2.isNamespaceImport(clause.namedBindings))
|
|
307
376
|
names.add(clause.namedBindings.name.text);
|
|
308
|
-
if (clause?.namedBindings &&
|
|
377
|
+
if (clause?.namedBindings && ts2.isNamedImports(clause.namedBindings))
|
|
309
378
|
for (const element of clause.namedBindings.elements)
|
|
310
379
|
names.add(element.name.text);
|
|
311
380
|
}
|
|
@@ -313,10 +382,10 @@ class SsrScanner {
|
|
|
313
382
|
}
|
|
314
383
|
#importsClientRuntime(sourceFile) {
|
|
315
384
|
for (const statement of sourceFile.statements) {
|
|
316
|
-
if (!
|
|
385
|
+
if (!ts2.isImportDeclaration(statement))
|
|
317
386
|
continue;
|
|
318
387
|
const bindings = statement.importClause?.namedBindings;
|
|
319
|
-
if (!bindings || !
|
|
388
|
+
if (!bindings || !ts2.isNamedImports(bindings))
|
|
320
389
|
continue;
|
|
321
390
|
if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
|
|
322
391
|
return true;
|
|
@@ -367,7 +436,7 @@ function getShare(serverMass, clientMass) {
|
|
|
367
436
|
return total === 0 ? 1 : serverMass / total;
|
|
368
437
|
}
|
|
369
438
|
function getSpecifier(statement) {
|
|
370
|
-
return
|
|
439
|
+
return ts2.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
|
|
371
440
|
}
|
|
372
441
|
function isVendorSpecifier(specifier) {
|
|
373
442
|
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
@@ -380,9 +449,9 @@ function isVendorSpecifier(specifier) {
|
|
|
380
449
|
}
|
|
381
450
|
function getAccessRoot(node) {
|
|
382
451
|
let current = node;
|
|
383
|
-
while (
|
|
452
|
+
while (ts2.isPropertyAccessExpression(current))
|
|
384
453
|
current = current.expression;
|
|
385
|
-
return
|
|
454
|
+
return ts2.isIdentifier(current) ? current.text : "";
|
|
386
455
|
}
|
|
387
456
|
|
|
388
457
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
@@ -406,6 +475,7 @@ var SUGGESTED_RULES = [
|
|
|
406
475
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
407
476
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
408
477
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
478
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a store action of the same name.",
|
|
409
479
|
"Move shared app utilities to apps/*/common instead of creating apps/*/base.",
|
|
410
480
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
|
|
411
481
|
];
|
|
@@ -429,6 +499,7 @@ var RULE_FIXES = {
|
|
|
429
499
|
"akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
430
500
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
431
501
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
502
|
+
"akan.agent.unpublished-form-setter": "Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a store action named set<Field>On<Model> that writes through super.set<Field>On<Model>(v).",
|
|
432
503
|
"akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
433
504
|
"akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
|
|
434
505
|
"akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
@@ -460,6 +531,7 @@ class AkanQualityScanner {
|
|
|
460
531
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
461
532
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
462
533
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
534
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
463
535
|
...ssr.warnings
|
|
464
536
|
];
|
|
465
537
|
return {
|
|
@@ -512,7 +584,7 @@ class AkanQualityScanner {
|
|
|
512
584
|
file,
|
|
513
585
|
absolutePath,
|
|
514
586
|
content,
|
|
515
|
-
sourceFile:
|
|
587
|
+
sourceFile: ts3.createSourceFile(file, content, ts3.ScriptTarget.Latest, true, getScriptKind(file))
|
|
516
588
|
};
|
|
517
589
|
}
|
|
518
590
|
async#readTextFile(workspaceRoot, file) {
|
|
@@ -759,7 +831,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
759
831
|
const declarations = [];
|
|
760
832
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
761
833
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
762
|
-
if (
|
|
834
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
763
835
|
declarations.push({
|
|
764
836
|
name: statement.name.text,
|
|
765
837
|
kind: "function",
|
|
@@ -769,7 +841,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
769
841
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
770
842
|
});
|
|
771
843
|
}
|
|
772
|
-
if (
|
|
844
|
+
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
773
845
|
declarations.push({
|
|
774
846
|
name: statement.name.text,
|
|
775
847
|
kind: "class",
|
|
@@ -779,9 +851,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
779
851
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
780
852
|
});
|
|
781
853
|
}
|
|
782
|
-
if (
|
|
854
|
+
if (ts3.isVariableStatement(statement) && isExported(statement)) {
|
|
783
855
|
for (const declaration of statement.declarationList.declarations) {
|
|
784
|
-
if (!
|
|
856
|
+
if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
785
857
|
continue;
|
|
786
858
|
declarations.push({
|
|
787
859
|
name: declaration.name.text,
|
|
@@ -820,14 +892,14 @@ function isInLibModule(file) {
|
|
|
820
892
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
821
893
|
}
|
|
822
894
|
function isEnumClassStatement(sourceFile, statement) {
|
|
823
|
-
if (!
|
|
895
|
+
if (!ts3.isClassDeclaration(statement))
|
|
824
896
|
return false;
|
|
825
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
897
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
826
898
|
const expression = heritageClause?.types[0]?.expression;
|
|
827
899
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
828
900
|
}
|
|
829
901
|
function getExportedClassNames(sourceFile) {
|
|
830
|
-
return sourceFile.statements.filter((statement) =>
|
|
902
|
+
return sourceFile.statements.filter((statement) => ts3.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
831
903
|
}
|
|
832
904
|
function isComponentDeclarationFile(file) {
|
|
833
905
|
if (!file.endsWith(".tsx"))
|
|
@@ -843,7 +915,7 @@ function isComponentDeclarationFile(file) {
|
|
|
843
915
|
function getComponentFileDeclarations(sourceFile) {
|
|
844
916
|
const reExportedNames = new Set;
|
|
845
917
|
for (const statement of sourceFile.statements) {
|
|
846
|
-
if (
|
|
918
|
+
if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
|
|
847
919
|
for (const element of statement.exportClause.elements)
|
|
848
920
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
849
921
|
}
|
|
@@ -853,19 +925,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
853
925
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
854
926
|
const inlineExported = isExported(statement);
|
|
855
927
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
856
|
-
if (
|
|
928
|
+
if (ts3.isInterfaceDeclaration(statement))
|
|
857
929
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
858
|
-
else if (
|
|
930
|
+
else if (ts3.isTypeAliasDeclaration(statement))
|
|
859
931
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
860
|
-
else if (
|
|
932
|
+
else if (ts3.isEnumDeclaration(statement))
|
|
861
933
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
862
|
-
else if (
|
|
934
|
+
else if (ts3.isFunctionDeclaration(statement) && statement.name)
|
|
863
935
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
864
|
-
else if (
|
|
936
|
+
else if (ts3.isClassDeclaration(statement) && statement.name)
|
|
865
937
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
866
|
-
else if (
|
|
938
|
+
else if (ts3.isVariableStatement(statement)) {
|
|
867
939
|
for (const declaration of statement.declarationList.declarations) {
|
|
868
|
-
if (!
|
|
940
|
+
if (!ts3.isIdentifier(declaration.name))
|
|
869
941
|
continue;
|
|
870
942
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
871
943
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -877,15 +949,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
877
949
|
function getCompoundComponentNames(sourceFile) {
|
|
878
950
|
const names = new Set;
|
|
879
951
|
for (const statement of sourceFile.statements) {
|
|
880
|
-
if (!
|
|
952
|
+
if (!ts3.isExpressionStatement(statement))
|
|
881
953
|
continue;
|
|
882
954
|
const { expression } = statement;
|
|
883
|
-
if (!
|
|
955
|
+
if (!ts3.isBinaryExpression(expression) || expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken)
|
|
884
956
|
continue;
|
|
885
|
-
if (!
|
|
957
|
+
if (!ts3.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
886
958
|
continue;
|
|
887
959
|
names.add(expression.left.name.text);
|
|
888
|
-
if (
|
|
960
|
+
if (ts3.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
889
961
|
names.add(expression.right.text);
|
|
890
962
|
}
|
|
891
963
|
return names;
|
|
@@ -905,9 +977,9 @@ function isPascalCaseName(name) {
|
|
|
905
977
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
906
978
|
}
|
|
907
979
|
function isDefaultExportStatement(statement) {
|
|
908
|
-
if (
|
|
980
|
+
if (ts3.isExportAssignment(statement))
|
|
909
981
|
return !statement.isExportEquals;
|
|
910
|
-
return !!(
|
|
982
|
+
return !!(ts3.getCombinedModifierFlags(statement) & ts3.ModifierFlags.Default);
|
|
911
983
|
}
|
|
912
984
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
913
985
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -937,7 +1009,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
937
1009
|
function getGlobalMutationWarnings(sourceFile) {
|
|
938
1010
|
const warnings = [];
|
|
939
1011
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
940
|
-
if (
|
|
1012
|
+
if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
941
1013
|
warnings.push({
|
|
942
1014
|
rule: "akan.file.global-declaration",
|
|
943
1015
|
scope: "file",
|
|
@@ -947,7 +1019,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
947
1019
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
948
1020
|
});
|
|
949
1021
|
}
|
|
950
|
-
if (
|
|
1022
|
+
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
951
1023
|
warnings.push({
|
|
952
1024
|
rule: "akan.file.window-augmentation",
|
|
953
1025
|
scope: "file",
|
|
@@ -957,7 +1029,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
957
1029
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
958
1030
|
});
|
|
959
1031
|
}
|
|
960
|
-
if (
|
|
1032
|
+
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
961
1033
|
warnings.push({
|
|
962
1034
|
rule: "akan.file.prototype-mutation",
|
|
963
1035
|
scope: "file",
|
|
@@ -975,23 +1047,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
975
1047
|
}
|
|
976
1048
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
977
1049
|
const line = getLine(sourceFile, statement);
|
|
978
|
-
if (
|
|
1050
|
+
if (ts3.isClassDeclaration(statement) && statement.name) {
|
|
979
1051
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
980
1052
|
}
|
|
981
|
-
if (
|
|
1053
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name) {
|
|
982
1054
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
983
1055
|
}
|
|
984
|
-
if (
|
|
1056
|
+
if (ts3.isInterfaceDeclaration(statement)) {
|
|
985
1057
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
986
1058
|
}
|
|
987
|
-
if (
|
|
1059
|
+
if (ts3.isTypeAliasDeclaration(statement)) {
|
|
988
1060
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
989
1061
|
}
|
|
990
|
-
if (
|
|
1062
|
+
if (ts3.isEnumDeclaration(statement)) {
|
|
991
1063
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
992
1064
|
}
|
|
993
|
-
if (
|
|
994
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1065
|
+
if (ts3.isVariableStatement(statement)) {
|
|
1066
|
+
return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => ({
|
|
995
1067
|
name: declaration.name.text,
|
|
996
1068
|
kind: "variable",
|
|
997
1069
|
line: getLine(sourceFile, declaration),
|
|
@@ -999,7 +1071,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
999
1071
|
node: statement
|
|
1000
1072
|
}));
|
|
1001
1073
|
}
|
|
1002
|
-
if (
|
|
1074
|
+
if (ts3.isExportDeclaration(statement)) {
|
|
1003
1075
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1004
1076
|
}
|
|
1005
1077
|
return [];
|
|
@@ -1024,9 +1096,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
1024
1096
|
return false;
|
|
1025
1097
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
1026
1098
|
return true;
|
|
1027
|
-
if (!
|
|
1099
|
+
if (!ts3.isClassDeclaration(declaration.node))
|
|
1028
1100
|
return false;
|
|
1029
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1101
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
1030
1102
|
const expression = heritageClause?.types[0]?.expression;
|
|
1031
1103
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
1032
1104
|
}
|
|
@@ -1092,13 +1164,13 @@ function getModuleInfo(file) {
|
|
|
1092
1164
|
return { moduleName, fileName, kind: "database" };
|
|
1093
1165
|
}
|
|
1094
1166
|
function isExportedConst(declaration) {
|
|
1095
|
-
return declaration.exported &&
|
|
1167
|
+
return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1096
1168
|
}
|
|
1097
1169
|
function isExported(node) {
|
|
1098
|
-
return !!
|
|
1170
|
+
return !!ts3.getCombinedModifierFlags(node) && !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
|
|
1099
1171
|
}
|
|
1100
1172
|
function isFunctionLikeInitializer(node) {
|
|
1101
|
-
return !!node && (
|
|
1173
|
+
return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
|
|
1102
1174
|
}
|
|
1103
1175
|
function getBodyFingerprint(sourceFile, node) {
|
|
1104
1176
|
if (!node)
|
|
@@ -1120,7 +1192,7 @@ async function isDirectory(absolutePath) {
|
|
|
1120
1192
|
}
|
|
1121
1193
|
}
|
|
1122
1194
|
function getScriptKind(file) {
|
|
1123
|
-
return file.endsWith(".tsx") ?
|
|
1195
|
+
return file.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
|
|
1124
1196
|
}
|
|
1125
1197
|
function getLine(sourceFile, node) {
|
|
1126
1198
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|