@akanjs/devkit 3.1.1-rc.8 → 3.1.1
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/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +30 -0
- package/akanConfig/akanConfig.ts +10 -0
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/applicationBuildRunner.test.ts +17 -0
- package/applicationBuildRunner.ts +3 -1
- package/biome.base.json +4 -1
- package/cloud/cloudUpdates.ts +3 -2
- package/codeAgent/agent/CodeAgent.ts +4 -0
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/akanSystemPrompt.ts +1 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +36 -0
- package/codeAgent/skills/akan-code/SKILL.md +1 -0
- package/codeAgent/skills/akan-debug/SKILL.md +40 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +38 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/akanCodePlugins.test.ts +60 -58
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/executors.test.ts +0 -4
- package/executors.ts +18 -2
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +94 -23
- package/mobile/desktopRelease.ts +83 -45
- package/mobile/nativeApi.test.ts +4 -1
- package/mobile/nativeApi.ts +3 -3
- package/mobile/nativeApp.test.ts +2 -2
- package/mobile/nativeApp.ts +18 -11
- package/mobile/nativeConfig.test.ts +16 -8
- package/mobile/nativeConfig.ts +1 -1
- package/mobile/updateUploader.test.ts +34 -0
- package/mobile/updateUploader.ts +9 -6
- package/package.json +3 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +1 -1
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +159 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +519 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +54 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-page
|
|
3
|
+
description: "Add or change a route under page/: the one page()/layout() chain, typed params and search, guards, heads, loading and errors, overrides, and lib pages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan pages
|
|
7
|
+
|
|
8
|
+
A route file exports one chain and nothing else. Every setting is a stage of it.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
export default page()
|
|
12
|
+
.param("orgId", ID)
|
|
13
|
+
.guard(() => signedIn())
|
|
14
|
+
.render(({ orgId }) => {
|
|
15
|
+
const { l } = usePage();
|
|
16
|
+
const { taskInitInOrg } = fetch.initTaskInOrg(orgId);
|
|
17
|
+
return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. Place the file: `<route>.tsx` or `<dir>/_index.tsx` serves the path, `_layout.tsx` nests a layout (`layout()`;
|
|
24
|
+
the root one is `rootLayout()`), `[id]` is a dynamic segment, `(group)` groups without a segment. `page/` holds
|
|
25
|
+
route modules only — no helpers, no `.ts` files.
|
|
26
|
+
2. Name every `[x]` segment with `.param("x", Type)` and read a query key only with `.search("k", Type)`. Values
|
|
27
|
+
arrive typed; `lang` reaches every stage and is never declared.
|
|
28
|
+
3. Gate with `.guard(() => signedIn())` on the `_layout.tsx` (a real redirect before anything renders), and repeat
|
|
29
|
+
a stricter guard on a page that needs more. The endpoint guards are still the real gate.
|
|
30
|
+
4. In `.render()`: `usePage()`, then fetch, then return. Hand `fetch.init*` / `view*` / `edit*` fields to sections
|
|
31
|
+
unawaited; await only what the shell reads, in one `Promise.all`.
|
|
32
|
+
5. `.head()` takes JSX (or a function returning it), never a metadata object. A canonical link comes only from the
|
|
33
|
+
page's own head.
|
|
34
|
+
6. Re-skin an `akanjs/ui` component for a subtree with `_overrides.tsx`:
|
|
35
|
+
`export default override({ Slot: AppComponent })`, no logic.
|
|
36
|
+
|
|
37
|
+
## Never
|
|
38
|
+
|
|
39
|
+
- A `fetch.<mutation>()` while rendering — it throws `base.error.mutationInRender`, because opening a link would
|
|
40
|
+
perform it. Send writes from a user action.
|
|
41
|
+
- `useState`, `useEffect`, comments, a named export beside the chain, or the legacy `export default async function`.
|
|
42
|
+
- Editing a lib page through its link in `page/(libs)/` — edit the lib's own `page/` source.
|
|
43
|
+
|
|
44
|
+
Full detail: `get_guideline` with `pageRule`, or `ssrRule` for what may render on the server.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-query
|
|
3
|
+
description: "Write filters, slices and list loading: query descriptors, init paging and loadMoreOf, soft removal and hookless writes, q.search text search, and the cascade direction."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan queries and slices
|
|
7
|
+
|
|
8
|
+
## Filters and slices
|
|
9
|
+
|
|
10
|
+
- A filter in `<model>.document.ts` declares its args; a slice in `<model>.signal.ts` serves it to stores. A slice
|
|
11
|
+
`exec` returns a `query<Filter>(…)` descriptor — never a `list<Filter>` array — and you cannot chain sort or
|
|
12
|
+
limit on it.
|
|
13
|
+
- Page through the store: `initX(…, { sort, page, limit })`, then `st.do.loadMoreOf<Model>()` while
|
|
14
|
+
`hasMoreOf<Model>`. Never compare `count > page * limit`.
|
|
15
|
+
- Never ask for more than 500 rows: the server serves at most 500 per request and drops the rest silently.
|
|
16
|
+
A figure over the whole set belongs in an insight or an endpoint.
|
|
17
|
+
- The root slice takes a filter name and its args, never a raw query, and its `root:` guard is always `Admin`.
|
|
18
|
+
- No value is `q.empty`, never `q.missing`.
|
|
19
|
+
- A projection is bare on the facade (`pickById(id, { secret: true })`) and under `select` on a filter accessor —
|
|
20
|
+
the only way to read a `field.secret` value.
|
|
21
|
+
|
|
22
|
+
## Writes and removal
|
|
23
|
+
|
|
24
|
+
- Removal is soft: reads see live rows unless `{ withRemoved: true }`. The one hard delete is `purge<Filter>` /
|
|
25
|
+
`purgeMany`, for data past retention.
|
|
26
|
+
- A filter's `remove*` / `update*`, `updateById` and `removeById` run no hooks and no cascade. A model with a
|
|
27
|
+
cascade or `_postRemove` removes through `remove<Model>(id)`.
|
|
28
|
+
- Load a set once (`load<Model>Many`, one filter query) instead of one call per item in a loop — lint fails a
|
|
29
|
+
per-item service call an endpoint reaches.
|
|
30
|
+
|
|
31
|
+
## Search and cascade
|
|
32
|
+
|
|
33
|
+
- `q.search(text, { prefix: true })` sits in a filter at an AND position, over fields with the `text` role. Blank
|
|
34
|
+
input matches nothing — never a passthrough. A search slice only over data safe to enumerate.
|
|
35
|
+
- `cascade` names a direction, and the wrong one loses data: `removeRef` on the owner's relation, `removeWith` on
|
|
36
|
+
the child's reference to its owner.
|
|
37
|
+
|
|
38
|
+
Full detail: `get_guideline` with `queryRule`, and `workspaceRecipes` for a worked slice.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-runtime
|
|
3
|
+
description: "Configure how an app runs: web surfaces, route prefixes, gateway vs solo, akan start and logs, the Docker image, native and desktop apps, database modes, ops, assets and the build pipeline."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan runtime
|
|
7
|
+
|
|
8
|
+
Runtime and build settings are declared in `akan.config.ts` and narrowed — never widened — by an env at boot.
|
|
9
|
+
Domain code reaches none of them.
|
|
10
|
+
|
|
11
|
+
## Where each setting lives
|
|
12
|
+
|
|
13
|
+
| Want | Setting |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| API only, or no CSR bundle for native apps | `web: false`, `web: { csr: false }` |
|
|
16
|
+
| Move the API or websocket mount | `new AkanApp({ prefix, websocketPrefix })` in `main.ts`, mirrored in `api: { … }`; read with `getApiPrefix()` / `getWsPrefix()` |
|
|
17
|
+
| The production image | `docker: string \| { image, preRuns, postRuns, command }` |
|
|
18
|
+
| iOS, Android, desktop | the `native` section; `desktop: { server: true }` carries the server |
|
|
19
|
+
| SQLite, several databases, a cluster | `database: { modes: [...] }`, picked by `AKAN_DATABASE_MODE` |
|
|
20
|
+
|
|
21
|
+
- `main.ts` imports `AkanApp` from `akanjs/server/akanApp`, never the barrel.
|
|
22
|
+
- Never write `/api` or `/ws` literally, and never pass a prefix as a prop.
|
|
23
|
+
- The generated image installs only `ca-certificates` and `tzdata`; add the rest in `preRuns`.
|
|
24
|
+
- An app running in two database modes avoids `q.raw`.
|
|
25
|
+
|
|
26
|
+
## Running and logs
|
|
27
|
+
|
|
28
|
+
- `akan start <app>` (`a,b` or `all` for several) runs the dev server; `akan build <app>` checks the production
|
|
29
|
+
build. A supervised session writes `local/apps/<app>/runtime/dev.log` — hand over that path, not a paste of the
|
|
30
|
+
live view. Tail with `akan logs <app>`.
|
|
31
|
+
- Give every log sink a floor (`Logger.addSink(sink, { minLevel })`) and never log once per delivered record.
|
|
32
|
+
|
|
33
|
+
## Native and desktop
|
|
34
|
+
|
|
35
|
+
- `akan start-desktop <app>`, `akan start-ios <app>`, `akan start-android <app>` load pages from a running
|
|
36
|
+
`akan start`; a desktop app carrying its server starts it itself.
|
|
37
|
+
- A desktop app's carried server listens on a loopback port any local program can call — guard it like a network
|
|
38
|
+
server. Read runtime files from `AKAN_APP_DIR`, and keep a user's secret with `NativeSecret`, never in a file.
|
|
39
|
+
|
|
40
|
+
Full detail: `get_guideline` with `runtimeRule`.
|
|
@@ -43,8 +43,8 @@ Each result names the next call in `next.tool`. Follow it; do not improvise.
|
|
|
43
43
|
and each workflow's inputs.)
|
|
44
44
|
2. `apply_workflow({ planPath })` — writes the scaffold and syncs the generated barrels. Returns
|
|
45
45
|
`validationTarget`.
|
|
46
|
-
3. `run_validation({ runIdOrPlan: <validationTarget> })`, or
|
|
47
|
-
|
|
46
|
+
3. `run_validation({ runIdOrPlan: <validationTarget> })`, or run `akan verify` (`akan_verify` in akan code);
|
|
47
|
+
both are the whole chain in one process.
|
|
48
48
|
4. **Read each scaffolded file before editing it.** They are a few lines each and they extend base classes
|
|
49
49
|
that already generated the CRUD surface. Read the `akan-store` skill before touching a store or adding
|
|
50
50
|
signal endpoints, or you will redeclare `create<Model>` and hit an "already exists" typecheck error.
|
|
@@ -60,9 +60,19 @@ Each result names the next call in `next.tool`. Follow it; do not improvise.
|
|
|
60
60
|
- Hand-writing files is the fallback, used only when no workflow matches, or when apply reports
|
|
61
61
|
unsupported / no-op / failed diagnostics telling you to act manually. Say which case applied.
|
|
62
62
|
- Never hand-edit a generated file to "finish" a scaffold — use `repair_generated` or `akan sync <app>`.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
|
|
64
|
+
## No workflow tools? The CLI is the same chain
|
|
65
|
+
|
|
66
|
+
The tools come from `akan mcp` (`--mode plan` reads, `--mode apply` writes). When they are not connected, each
|
|
67
|
+
has a command that prints the same report:
|
|
68
|
+
|
|
69
|
+
| tool | command |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `list_workflows` / `explain_workflow` | `akan workflow list` / `akan workflow explain <name>` |
|
|
72
|
+
| `plan_workflow` | `akan workflow plan create-module --app <app> --module comment --format json --out <path>` |
|
|
73
|
+
| `apply_workflow` | `akan workflow apply <path> --format json` (`--dry-run` previews) |
|
|
74
|
+
| `run_validation` | `akan verify <app>` |
|
|
75
|
+
| `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <app>` |
|
|
66
76
|
|
|
67
77
|
## Worked example — "an intro page, two detail pages, and a comment zone with a database"
|
|
68
78
|
|
|
@@ -73,4 +83,4 @@ Each result names the next call in `next.tool`. Follow it; do not improvise.
|
|
|
73
83
|
leave the store body empty unless you need extra state.
|
|
74
84
|
4. Write the pages and the comment UI against the generated actions — `st.do.createComment`,
|
|
75
85
|
`st.do.initCommentInPublic`, `st.use.commentListInPublic`.
|
|
76
|
-
5. `
|
|
86
|
+
5. `akan verify` until it prints `verify ok`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-transport
|
|
3
|
+
description: "Guard and tune endpoints on the wire: writing a Can<Verb><Model> guard, deadlines, cache, idempotency, rate limits, server-to-server calls, body types, bytes and HTTP verbs."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan transport
|
|
7
|
+
|
|
8
|
+
## Guards
|
|
9
|
+
|
|
10
|
+
- Every custom `query` / `mutation` / `pubsub` / `message` names its own `guards: [...]`; every `slice()` takes an
|
|
11
|
+
explicit guard map with `root: Admin`. `Public` belongs on a slice `get:`, and on a mutation only for an act that
|
|
12
|
+
must work signed out (sign-up, sign-in, token refresh, an OAuth route), which also declares a `rateLimit`.
|
|
13
|
+
- A resource guard is a `Can<Verb><Model>` class in the model library's `srvkit/guards.ts` that fails closed:
|
|
14
|
+
no resource named, or a load that throws, is `false` (after a `logger.warn`). Admin bypass goes first.
|
|
15
|
+
- Every guard declares `static scope`: `"account"` reads only the caller (`SignedIn`, `Admin`, roles), and
|
|
16
|
+
`"resource"` needs the call's arguments (every `Can<Verb><Model>`). Keep `static name = "User";` with a comment.
|
|
17
|
+
- Guards read `context.get<T>("account")`, serve HTTP and websocket alike, and stay side-effect free. The acting
|
|
18
|
+
user comes from `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`, the address from `.with(Ip)` — never a
|
|
19
|
+
client-supplied id. The service re-checks ownership: two independent gates.
|
|
20
|
+
|
|
21
|
+
## Endpoint options
|
|
22
|
+
|
|
23
|
+
- `{ timeout: <ms> }` is the deadline on both ends; undeclared, the client quits at 30 s and the handler is not
|
|
24
|
+
cancelled.
|
|
25
|
+
- `{ cache }` only on a `query` with no internal argument, capped at one day; `invalidate(endpoint, args?)` drops
|
|
26
|
+
it early through a plugged `EndpointCacheControl`.
|
|
27
|
+
- `{ rateLimit: { calls, windowMs?, by? } }` per endpoint, `option.setRateLimit` per app, `plug(RateLimit)` for a
|
|
28
|
+
business key.
|
|
29
|
+
- A mutation repeated under one `Idempotency-Key` header runs once and replays its first answer.
|
|
30
|
+
- `{ method: "PATCH" | "PUT" | "DELETE" }` moves a mutation off `POST` — only when a foreign protocol forces it.
|
|
31
|
+
- `errors: { 409: ["ticket.error.locked"] }` names the errors an endpoint throws on purpose.
|
|
32
|
+
|
|
33
|
+
## Bodies and other servers
|
|
34
|
+
|
|
35
|
+
- `.body()` / `.param()` take scalars, model refs or `enumOf(...)`; `Int` / `Float`, never `Number`; bytes are
|
|
36
|
+
`Binary`, never `Any`; `Upload` only in a `fileUpload: true` mutation.
|
|
37
|
+
- Server to server, `fetch.clone({ origin })` opens no socket without `connect: true`, retries through `retry`,
|
|
38
|
+
and rethrows a remote `Err` as it is — never wrap or re-key it.
|
|
39
|
+
|
|
40
|
+
Full detail: `get_guideline` with `transportRule`, or `mcpRule` for what a guard's scope does to an MCP listing.
|
|
@@ -7,17 +7,19 @@ description: "Run the akan check loop after editing and fix each failure class t
|
|
|
7
7
|
|
|
8
8
|
## One call does the whole chain
|
|
9
9
|
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `akan
|
|
19
|
-
- `akan
|
|
20
|
-
- `akan build
|
|
10
|
+
`akan verify` (the `akan_verify` tool in akan code) syncs, lints with fixes, typechecks, fast-builds each
|
|
11
|
+
changed app, and runs the SSR check for a `.tsx` change — over everything the working tree has changed, in one
|
|
12
|
+
process. It prints one line when it passes and one line per problem when it fails. **Run it after editing,
|
|
13
|
+
before you say you are done.** In akan code it also runs itself at the end of a turn that changed source.
|
|
14
|
+
|
|
15
|
+
Never run `akan lint`, `akan typecheck` and `akan build` in turn: that is the same work three times over, at
|
|
16
|
+
three times the output. Narrow the one command instead:
|
|
17
|
+
|
|
18
|
+
- `akan verify <app-or-lib-or-pkg>` — that target whole, changed or not.
|
|
19
|
+
- `akan verify --lint-only` — the quick pass between edits.
|
|
20
|
+
- `akan verify --no-build` — leave the app build out.
|
|
21
|
+
- `akan verify --max 0` — every line, when the report ends in `+N more`.
|
|
22
|
+
- `fixed N files` in the header means lint rewrote them on disk: re-read before editing them again.
|
|
21
23
|
|
|
22
24
|
Raw `tsc` misses the generated barrels and reports false errors. Do not reach for it.
|
|
23
25
|
|
|
@@ -24,7 +24,8 @@ export class AkanCli {
|
|
|
24
24
|
cwd: this.#cwd,
|
|
25
25
|
stdout: "pipe",
|
|
26
26
|
stderr: "pipe",
|
|
27
|
-
|
|
27
|
+
//? A workspace .env's AKAN_PUBLIC_LOG_LEVEL=verbose reaches the child too, and buried its error under 90 lines.
|
|
28
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", AKAN_LOG_STDOUT_LEVEL: "warn" },
|
|
28
29
|
});
|
|
29
30
|
const timer = setTimeout(() => proc.kill(), timeoutMs);
|
|
30
31
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { CodeAgentMcpStatus, CodeAgentProfile, CodeAgentQuestion, CodeAgentSubagent } from "akanjs/common";
|
|
3
3
|
import type { Workspace } from "../../commandDecorators";
|
|
4
|
+
import { PreviewView } from "../../preview/PreviewView";
|
|
5
|
+
import type { CodeAgentModelRef } from "../agent/akanCodeModel";
|
|
4
6
|
import type { CodeMailbox } from "../agent/CodeMailbox";
|
|
5
7
|
import { SubagentPool, type SubagentSpend } from "../agent/SubagentPool";
|
|
6
8
|
import { DevLogFeedback } from "../feedback/DevLogFeedback";
|
|
7
9
|
import { PreviewFeedback } from "../feedback/PreviewFeedback";
|
|
8
|
-
import { PreviewView } from "../feedback/PreviewView";
|
|
9
10
|
import { TurnFeedback, type TurnFeedbackSource } from "../feedback/TurnFeedback";
|
|
10
11
|
import { VerifyFeedback } from "../feedback/VerifyFeedback";
|
|
11
12
|
import { AkanToolPack } from "./AkanToolPack";
|
|
@@ -23,6 +24,8 @@ export interface AkanCodePluginsOptions {
|
|
|
23
24
|
/** Apps whose dev server should be watched, and whose page should be probed. */
|
|
24
25
|
apps: string[];
|
|
25
26
|
canSeeImages: boolean;
|
|
27
|
+
/** The owning agent's model as it is now; a `task` child runs on it. */
|
|
28
|
+
model?: () => CodeAgentModelRef | undefined;
|
|
26
29
|
deepseek?: DeepSeekWeb | undefined;
|
|
27
30
|
/** How many `task` levels deep the owning agent is. */
|
|
28
31
|
depth: number;
|
|
@@ -92,6 +95,7 @@ export class AkanCodePlugins {
|
|
|
92
95
|
workspace: options.workspace,
|
|
93
96
|
cwd: options.cwd,
|
|
94
97
|
parent: options.profile,
|
|
98
|
+
...(options.model ? { model: options.model } : {}),
|
|
95
99
|
depth: options.depth,
|
|
96
100
|
...(options.subagentSpend ? { spend: options.subagentSpend } : {}),
|
|
97
101
|
...(options.onNotice ? { onNotice: options.onNotice } : {}),
|
|
@@ -2,12 +2,13 @@ export interface AkanEditScopeResult {
|
|
|
2
2
|
paths: string[];
|
|
3
3
|
apps: string[];
|
|
4
4
|
libs: string[];
|
|
5
|
+
pkgs: string[];
|
|
5
6
|
/** A created, deleted or renamed file under `apps/**` or `libs/**` — the only kind a barrel has to catch up with. */
|
|
6
7
|
needsSync: boolean;
|
|
7
8
|
touchesTsx: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const empty: AkanEditScopeResult = { paths: [], apps: [], libs: [], needsSync: false, touchesTsx: false };
|
|
11
|
+
const empty: AkanEditScopeResult = { paths: [], apps: [], libs: [], pkgs: [], needsSync: false, touchesTsx: false };
|
|
11
12
|
|
|
12
13
|
// Read from git, not tool events: an edit made through `bash` (a heredoc, sed, a codemod) produces no write tool call.
|
|
13
14
|
export class AkanEditScope {
|
|
@@ -21,6 +22,7 @@ export class AkanEditScope {
|
|
|
21
22
|
paths,
|
|
22
23
|
apps: AkanEditScope.#targets(paths, "apps"),
|
|
23
24
|
libs: AkanEditScope.#targets(paths, "libs"),
|
|
25
|
+
pkgs: AkanEditScope.#targets(paths, "pkgs"),
|
|
24
26
|
needsSync: entries.some((entry) => /[ARD?]/.test(entry.code) && /^(apps|libs)\//.test(entry.file)),
|
|
25
27
|
touchesTsx: paths.some((file) => file.endsWith(".tsx")),
|
|
26
28
|
};
|
|
@@ -38,10 +40,13 @@ export class AkanEditScope {
|
|
|
38
40
|
return new Set(text.split("\n").filter((line) => line.trim().length > 3));
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
static #targets(paths: string[], root: "apps" | "libs") {
|
|
43
|
+
static #targets(paths: string[], root: "apps" | "libs" | "pkgs") {
|
|
42
44
|
const names = paths
|
|
43
45
|
.filter((file) => file.startsWith(`${root}/`))
|
|
44
|
-
.map((file) =>
|
|
46
|
+
.map((file) => {
|
|
47
|
+
const [, first = "", second = ""] = file.split("/");
|
|
48
|
+
return first.startsWith("@") && second ? `${first}/${second}` : first;
|
|
49
|
+
})
|
|
45
50
|
.filter(Boolean);
|
|
46
51
|
return [...new Set(names)];
|
|
47
52
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import type { Workspace } from "../../commandDecorators";
|
|
4
|
-
import { AkanEditScope } from "./AkanEditScope";
|
|
5
4
|
import { AkanVerifier } from "./AkanVerifier";
|
|
6
5
|
|
|
7
6
|
export interface AkanToolPackOptions {
|
|
@@ -66,17 +65,12 @@ export class AkanToolPack {
|
|
|
66
65
|
name: AkanToolPack.verifyToolName,
|
|
67
66
|
label: "akan verify",
|
|
68
67
|
description:
|
|
69
|
-
"Run the
|
|
70
|
-
promptSnippet: "akan_verify:
|
|
68
|
+
"Run `akan verify` over everything the working tree has changed: sync, lint (with fixes), typecheck, a fast build of each changed app, and the SSR check for .tsx changes, in one call. It prints one line when it passes and one line per problem when it fails. Call this after editing, before you finish; never run lint, typecheck or build separately.",
|
|
69
|
+
promptSnippet: "akan_verify: sync, lint, typecheck and build what the working tree changed, in one call",
|
|
71
70
|
parameters: Type.Object({}),
|
|
72
71
|
execute: async () => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
content: [{ type: "text", text: AkanVerifier.summarize(report) }],
|
|
77
|
-
details: undefined,
|
|
78
|
-
isError: !report.ok,
|
|
79
|
-
};
|
|
72
|
+
const report = await verifier.verify();
|
|
73
|
+
return { content: [{ type: "text", text: report.text }], details: undefined, isError: !report.ok };
|
|
80
74
|
},
|
|
81
75
|
});
|
|
82
76
|
}
|
|
@@ -1,48 +1,30 @@
|
|
|
1
1
|
import { AkanCli, type AkanCliResult } from "./AkanCli";
|
|
2
|
-
import type { AkanEditScopeResult } from "./AkanEditScope";
|
|
3
2
|
|
|
4
3
|
export interface AkanVerifyReport {
|
|
5
4
|
ok: boolean;
|
|
6
|
-
|
|
7
|
-
scope: AkanEditScopeResult;
|
|
5
|
+
text: string;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
export class AkanVerifier {
|
|
9
|
+
static readonly tailLines = 20;
|
|
11
10
|
readonly #cli: AkanCli;
|
|
12
11
|
|
|
13
12
|
constructor(cwd: string) {
|
|
14
13
|
this.#cli = new AkanCli(cwd);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
async verify(
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
if (scope.needsSync) for (const target of targets) steps.push(await this.#cli.run(["sync", target]));
|
|
22
|
-
for (const target of targets) steps.push(await this.#cli.run(["lint", target]));
|
|
23
|
-
for (const app of scope.apps) steps.push(await this.#cli.run(["typecheck", app]));
|
|
24
|
-
if (scope.touchesTsx) steps.push(await this.#cli.run(["quality", "ssr"]));
|
|
25
|
-
return { ok: steps.every((step) => step.ok), steps, scope };
|
|
16
|
+
async verify(paths: string[] = []): Promise<AkanVerifyReport> {
|
|
17
|
+
const args = ["verify", ...(paths.length ? ["--files", paths.join(",")] : [])];
|
|
18
|
+
const result = await this.#cli.run(args, { timeoutMs: 600_000 });
|
|
19
|
+
return { ok: result.ok, text: AkanVerifier.summarize(result) };
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
static summarize(
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
`Verification failed: ${failed.map((step) => step.command).join(", ")}.`,
|
|
37
|
-
passed.length ? `Passed: ${passed.join(", ")}.` : "",
|
|
38
|
-
...blocks,
|
|
39
|
-
]
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.join("\n\n");
|
|
22
|
+
//* `akan verify` already prints the agent-sized report; only a crash before it prints needs trimming.
|
|
23
|
+
static summarize(result: AkanCliResult) {
|
|
24
|
+
if (result.output.startsWith("verify ")) return result.output;
|
|
25
|
+
const lines = result.output.split("\n");
|
|
26
|
+
const tail =
|
|
27
|
+
lines.length <= AkanVerifier.tailLines ? result.output : `…\n${lines.slice(-AkanVerifier.tailLines).join("\n")}`;
|
|
28
|
+
return `${result.command} exited ${result.exitCode} before reporting:\n${tail}`;
|
|
42
29
|
}
|
|
43
30
|
}
|
|
44
|
-
|
|
45
|
-
const tail = (text: string, lines: number) => {
|
|
46
|
-
const all = text.split("\n");
|
|
47
|
-
return all.length <= lines ? text : `…\n${all.slice(-lines).join("\n")}`;
|
|
48
|
-
};
|
|
@@ -5,7 +5,6 @@ import { makeCliTempWorkspace, tempRoots, writeText } from "../../testHelpers";
|
|
|
5
5
|
import { CodeAgent, type CodeAgentOptions } from "../agent/CodeAgent";
|
|
6
6
|
import { SubagentPool } from "../agent/SubagentPool";
|
|
7
7
|
import { DevLogFeedback } from "../feedback/DevLogFeedback";
|
|
8
|
-
import { PreviewView } from "../feedback/PreviewView";
|
|
9
8
|
import { TurnFeedback, type TurnFeedbackSource } from "../feedback/TurnFeedback";
|
|
10
9
|
import { AkanCodePlugins } from "./AkanCodePlugins";
|
|
11
10
|
import { AkanEditScope } from "./AkanEditScope";
|
|
@@ -111,38 +110,18 @@ describe("TurnFeedback", () => {
|
|
|
111
110
|
});
|
|
112
111
|
|
|
113
112
|
describe("AkanVerifier.summarize", () => {
|
|
114
|
-
|
|
113
|
+
test("passes the verify report through untouched", () => {
|
|
114
|
+
const output =
|
|
115
|
+
"verify fail · a · lint ok · typecheck 1 error · 4.1s\napps/a/x.ts:1:7 TS2322 Type 'string' is not assignable to type 'number'.";
|
|
116
|
+
expect(AkanVerifier.summarize({ command: "akan verify", ok: false, exitCode: 1, output })).toBe(output);
|
|
117
|
+
});
|
|
115
118
|
|
|
116
|
-
test("
|
|
119
|
+
test("keeps only the tail of a crash that never reached the report", () => {
|
|
117
120
|
const output = [...Array(100).keys()].map((n) => `line ${n}`).join("\n");
|
|
118
|
-
const text = AkanVerifier.summarize(
|
|
119
|
-
|
|
120
|
-
ok: false,
|
|
121
|
-
scope,
|
|
122
|
-
steps: [
|
|
123
|
-
{ command: "akan lint a", ok: true, exitCode: 0, output: "" },
|
|
124
|
-
{ command: "akan typecheck a", ok: false, exitCode: 1, output },
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
{ tailLines: 5 },
|
|
128
|
-
);
|
|
129
|
-
expect(text).toContain("Verification failed: akan typecheck a");
|
|
130
|
-
expect(text).toContain("Passed: akan lint a");
|
|
121
|
+
const text = AkanVerifier.summarize({ command: "akan verify", ok: false, exitCode: 1, output });
|
|
122
|
+
expect(text).toStartWith("akan verify exited 1 before reporting:");
|
|
131
123
|
expect(text).toContain("line 99");
|
|
132
|
-
expect(text).not.toContain("line
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("says so plainly when everything passed", () => {
|
|
136
|
-
const text = AkanVerifier.summarize({
|
|
137
|
-
ok: true,
|
|
138
|
-
scope,
|
|
139
|
-
steps: [{ command: "akan lint a", ok: true, exitCode: 0, output: "" }],
|
|
140
|
-
});
|
|
141
|
-
expect(text).toContain("Verification passed");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("an untouched workspace is not a failure", () => {
|
|
145
|
-
expect(AkanVerifier.summarize({ ok: true, scope, steps: [] })).toContain("nothing to verify");
|
|
124
|
+
expect(text).not.toContain("line 79\n");
|
|
146
125
|
});
|
|
147
126
|
});
|
|
148
127
|
|
|
@@ -153,9 +132,11 @@ describe("AkanEditScope", () => {
|
|
|
153
132
|
const baseline = await AkanEditScope.baseline(root);
|
|
154
133
|
await writeText(path.join(root, "apps", "demo", "lib", "post", "post.service.ts"), "export {};\n");
|
|
155
134
|
await writeText(path.join(root, "libs", "shared", "ui", "Card.tsx"), "export const Card = () => null;\n");
|
|
135
|
+
await writeText(path.join(root, "pkgs", "@akanjs", "cli", "index.ts"), "export {};\n");
|
|
156
136
|
const scope = await AkanEditScope.since(root, baseline);
|
|
157
137
|
expect(scope.apps).toEqual(["demo"]);
|
|
158
138
|
expect(scope.libs).toEqual(["shared"]);
|
|
139
|
+
expect(scope.pkgs).toEqual(["@akanjs/cli"]);
|
|
159
140
|
expect(scope.needsSync).toBe(true);
|
|
160
141
|
expect(scope.touchesTsx).toBe(true);
|
|
161
142
|
});
|
|
@@ -165,7 +146,7 @@ describe("AkanEditScope", () => {
|
|
|
165
146
|
await Bun.spawn(["git", "init", "-q"], { cwd: root, stdout: "ignore", stderr: "ignore" }).exited;
|
|
166
147
|
const baseline = await AkanEditScope.baseline(root);
|
|
167
148
|
const scope = await AkanEditScope.since(root, baseline);
|
|
168
|
-
expect(scope).toEqual({ paths: [], apps: [], libs: [], needsSync: false, touchesTsx: false });
|
|
149
|
+
expect(scope).toEqual({ paths: [], apps: [], libs: [], pkgs: [], needsSync: false, touchesTsx: false });
|
|
169
150
|
});
|
|
170
151
|
});
|
|
171
152
|
|
|
@@ -243,33 +224,6 @@ describe("AkanEnvKeys", () => {
|
|
|
243
224
|
});
|
|
244
225
|
});
|
|
245
226
|
|
|
246
|
-
describe("PreviewView", () => {
|
|
247
|
-
test("reports whether this Bun build can drive a headless page at all", () => {
|
|
248
|
-
expect(typeof PreviewView.available).toBe("boolean");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test.skipIf(!PreviewView.available)(
|
|
252
|
-
"finds an empty body and a console error on a real page",
|
|
253
|
-
async () => {
|
|
254
|
-
const server = Bun.serve({
|
|
255
|
-
port: 0,
|
|
256
|
-
fetch: () =>
|
|
257
|
-
new Response('<!doctype html><html><body></body><script>console.error("boom")</script></html>', {
|
|
258
|
-
headers: { "content-type": "text/html" },
|
|
259
|
-
}),
|
|
260
|
-
});
|
|
261
|
-
try {
|
|
262
|
-
const probe = await PreviewView.probe(`http://localhost:${server.port}/`);
|
|
263
|
-
expect(probe.pageErrors.join(" ")).toContain("rendered empty");
|
|
264
|
-
expect(probe.consoleErrors.join(" ")).toContain("boom");
|
|
265
|
-
} finally {
|
|
266
|
-
server.stop(true);
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
30_000,
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
227
|
describe("SubagentPool", () => {
|
|
274
228
|
const workspace = { workspaceRoot: "/tmp/akan" } as never;
|
|
275
229
|
|
|
@@ -519,6 +473,54 @@ describe("SubagentPool", () => {
|
|
|
519
473
|
}
|
|
520
474
|
});
|
|
521
475
|
|
|
476
|
+
test("a child runs on the model its parent runs on when the task starts", async () => {
|
|
477
|
+
const { opened, spy } = openFakeAgents([]);
|
|
478
|
+
let current = { provider: "deepseek", id: "deepseek-v4-pro" };
|
|
479
|
+
try {
|
|
480
|
+
const root = SubagentPool.extensionFor({
|
|
481
|
+
workspace,
|
|
482
|
+
cwd: "/tmp/akan",
|
|
483
|
+
parent: budgeted(1_000),
|
|
484
|
+
model: () => current,
|
|
485
|
+
depth: 0,
|
|
486
|
+
});
|
|
487
|
+
await runTask(root, "c1");
|
|
488
|
+
current = { provider: "openai", id: "gpt-5.5" };
|
|
489
|
+
await runTask(root, "c2");
|
|
490
|
+
expect(opened.map((options) => options.model)).toEqual([
|
|
491
|
+
{ provider: "deepseek", id: "deepseek-v4-pro" },
|
|
492
|
+
{ provider: "openai", id: "gpt-5.5" },
|
|
493
|
+
]);
|
|
494
|
+
} finally {
|
|
495
|
+
spy.mockRestore();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("the plugins hand their pool the owning agent's model", async () => {
|
|
500
|
+
const { opened, spy } = openFakeAgents([]);
|
|
501
|
+
try {
|
|
502
|
+
const plugins = await AkanCodePlugins.build({
|
|
503
|
+
workspace,
|
|
504
|
+
cwd: "/tmp/akan",
|
|
505
|
+
profile: budgeted(1_000),
|
|
506
|
+
apps: [],
|
|
507
|
+
canSeeImages: false,
|
|
508
|
+
model: () => ({ provider: "deepseek", id: "deepseek-v4-pro" }),
|
|
509
|
+
depth: 0,
|
|
510
|
+
currentSessionId: () => "",
|
|
511
|
+
mailbox: () => undefined,
|
|
512
|
+
});
|
|
513
|
+
await runTask(
|
|
514
|
+
plugins.extensions.find((extension) => extension.name === "akan-subagent"),
|
|
515
|
+
"c1",
|
|
516
|
+
);
|
|
517
|
+
expect(opened[0]?.model).toEqual({ provider: "deepseek", id: "deepseek-v4-pro" });
|
|
518
|
+
plugins.dispose();
|
|
519
|
+
} finally {
|
|
520
|
+
spy.mockRestore();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
522
524
|
test("a child keeps no session and cannot ask a human", () => {
|
|
523
525
|
const child = SubagentPool.childOf(codeAgentPresets.local("/tmp/akan"), "explore");
|
|
524
526
|
expect(child.session).toEqual({ store: "memory", crossSession: false });
|
|
@@ -23,14 +23,24 @@ describe("WorkspaceVersionCheck", () => {
|
|
|
23
23
|
expect(WorkspaceVersionCheck.warning("3.1.1-rc.5", argv("sync", "demo"), await makeWorkspace({}))).toBeNull();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test("names both versions and the matching CLI with the same arguments", async () => {
|
|
26
|
+
test("names both versions and the matching CLI with the same arguments, quoted for the shell it runs in", async () => {
|
|
27
27
|
const root = await makeWorkspace({ akanjs: "3.1.0-rc.6" });
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const platform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
29
|
+
const warningOn = (os: NodeJS.Platform) => {
|
|
30
|
+
Object.defineProperty(process, "platform", { value: os });
|
|
31
|
+
return WorkspaceVersionCheck.warning("3.1.0", argv("sync", "demo", "--title", "it's"), root);
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const posix = warningOn("linux");
|
|
35
|
+
expect(posix).toContain("@akanjs/cli 3.1.0, but this workspace has akanjs 3.1.0-rc.6");
|
|
36
|
+
expect(posix).toContain(`Run the matching CLI: bunx -p @akanjs/cli@3.1.0-rc.6 akan sync demo --title 'it'\\''s'`);
|
|
37
|
+
expect(posix).toEndWith("together: akan update");
|
|
38
|
+
expect(warningOn("win32")).toContain(
|
|
39
|
+
`Run the matching CLI: bunx -p @akanjs/cli@3.1.0-rc.6 akan sync demo --title "it's"`,
|
|
40
|
+
);
|
|
41
|
+
} finally {
|
|
42
|
+
if (platform) Object.defineProperty(process, "platform", platform);
|
|
43
|
+
}
|
|
34
44
|
});
|
|
35
45
|
|
|
36
46
|
test("offers akan update with the CLI's prerelease tag only when the CLI is the newer one", async () => {
|
|
@@ -45,6 +45,9 @@ export class WorkspaceVersionCheck {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
static #quote(arg: string) {
|
|
48
|
-
|
|
48
|
+
if (/^[\w@%+=:,./-]+$/.test(arg)) return arg;
|
|
49
|
+
//? cmd and PowerShell take neither single quotes nor '\'' as POSIX shells do.
|
|
50
|
+
if (process.platform === "win32") return `"${arg.replaceAll('"', '\\"')}"`;
|
|
51
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
49
52
|
}
|
|
50
53
|
}
|