howtofixcli 0.7.8 → 0.7.10
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/dist/chunk-27TRKD65.js +2 -0
- package/dist/chunk-2YYQ3OBQ.js +1 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-3KT55SQP.js +34 -0
- package/dist/chunk-4BWDGEXH.js +5 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-ACOMNDUL.js +185 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-CGHISVXB.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-FRGMJPNO.js +2 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MTNY7MFG.js +6 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PIEAJJRE.js +5 -0
- package/dist/chunk-RDRF7DIS.js +109 -0
- package/dist/chunk-TEW62VDF.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-VICEM5Y2.js +2 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-WZKWT757.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +16 -63
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -120
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -301
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -288
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -715
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -347
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -446
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4757
- package/dist/update.js +0 -533
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- package/dist/workflow.js +0 -349
package/dist/prompt-files.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standing instructions the user can add without rebuilding the CLI.
|
|
3
|
-
*
|
|
4
|
-
* The CLI ships its own rules (`brand.ts` SYSTEM_APPEND). A user who wants a
|
|
5
|
-
* standing rule of their own — a house convention, a language to answer in, a
|
|
6
|
-
* command that must never run — otherwise has to edit the source or repeat
|
|
7
|
-
* themselves every session.
|
|
8
|
-
*
|
|
9
|
-
* So: `APPEND_SYSTEM.md` in the user's config root, appended after the built-in
|
|
10
|
-
* rules.
|
|
11
|
-
*
|
|
12
|
-
* <AGENT_DIR>/APPEND_SYSTEM.md
|
|
13
|
-
*
|
|
14
|
-
* **User scope only, deliberately.** A project-scoped file was written first and
|
|
15
|
-
* then removed, for two reasons that both held:
|
|
16
|
-
*
|
|
17
|
-
* - It would break prompt caching. The SDK renders `addendum` *before*
|
|
18
|
-
* `project_context`, and `project_context` is where the provider's cache
|
|
19
|
-
* breakpoint lands. A cwd-dependent file inside `addendum` therefore gives
|
|
20
|
-
* every worktree a different cached prefix — exactly the cost the ordering
|
|
21
|
-
* its blocks to avoid.
|
|
22
|
-
* - It would duplicate `AGENTS.md`, which the SDK already discovers per project
|
|
23
|
-
* and renders into `project_context`. Two files for one job means two sets of
|
|
24
|
-
* rules that can disagree, with no way to tell which won.
|
|
25
|
-
*
|
|
26
|
-
* Also deliberately NOT implemented: a file that *replaces* the prompt. The SDK's
|
|
27
|
-
* `customPrompt` swaps out the whole `preamble` section, and that section carries
|
|
28
|
-
* the tool list, the per-tool rules and the docs block — so a user file could
|
|
29
|
-
* silently take the agent's tools away with it. Replacing is a bigger contract
|
|
30
|
-
* than it looks. The append route has no such failure mode: it can only add.
|
|
31
|
-
*/
|
|
32
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
33
|
-
import { join } from "node:path";
|
|
34
|
-
import { AGENT_DIR } from "./paths.js";
|
|
35
|
-
/** The user-scoped file. Named here so the check script and the docs agree. */
|
|
36
|
-
export const USER_PROMPT_FILE = join(AGENT_DIR, "APPEND_SYSTEM.md");
|
|
37
|
-
/**
|
|
38
|
-
* Stated to the model only when no one can answer it.
|
|
39
|
-
*
|
|
40
|
-
* A bundled skill resolves its mode by asking the user and stopping until they
|
|
41
|
-
* reply. In a terminal that is the right behaviour. Under `--print` there is no
|
|
42
|
-
* one to reply, so the question becomes a dead end: the model asked, the run
|
|
43
|
-
* ended, and the task it was given was never started. The skill cannot know the
|
|
44
|
-
* difference and the prompt never said, so the run looks successful — exit 0,
|
|
45
|
-
* no error — while producing nothing.
|
|
46
|
-
*
|
|
47
|
-
* This block closes that gap. It names the tools that stop and wait, so the
|
|
48
|
-
* model takes the sensible default instead of asking, and it says why, so it
|
|
49
|
-
* does not read the instruction to ask as one it should follow anyway.
|
|
50
|
-
*/
|
|
51
|
-
export const NON_INTERACTIVE_BLOCK = [
|
|
52
|
-
"<non-interactive>",
|
|
53
|
-
"This run has no user attached: it was started with --print, and there is nobody to answer a question. Anything that stops and waits for a reply — `ask`, a confirmation, a skill that resolves a mode by asking which mode to use — will simply end the run with the task unfinished.",
|
|
54
|
-
"So do not ask. Where an instruction tells you to ask the user and wait, take the default it describes and continue, and say which default you took and why in your final answer. The user reads that answer; they cannot answer mid-run.",
|
|
55
|
-
"Where a skill offers a choice of modes, prefer the one that applies the rules while you work over the one that audits afterwards: this is the mode for building something now, and the audit mode can still be run later by the same person who reads your answer.",
|
|
56
|
-
"</non-interactive>",
|
|
57
|
-
].join("\n");
|
|
58
|
-
/**
|
|
59
|
-
* A user file is prose, not a payload. One pasted log or a stray directory dump
|
|
60
|
-
* would otherwise ride along in every single request; past this size the file is
|
|
61
|
-
* skipped rather than truncated, because half a rule is a different rule.
|
|
62
|
-
*/
|
|
63
|
-
const MAX_BYTES = 16 * 1024;
|
|
64
|
-
/**
|
|
65
|
-
* Read the file, or `undefined`. Never throws: an unreadable or oversized file
|
|
66
|
-
* must not stop a session from opening, and a permissions error is the user's to
|
|
67
|
-
* fix rather than a reason to refuse to work.
|
|
68
|
-
*/
|
|
69
|
-
export function readPromptFile(path) {
|
|
70
|
-
try {
|
|
71
|
-
if (!existsSync(path))
|
|
72
|
-
return undefined;
|
|
73
|
-
const text = readFileSync(path, "utf8").trim();
|
|
74
|
-
if (!text)
|
|
75
|
-
return undefined;
|
|
76
|
-
if (Buffer.byteLength(text, "utf8") > MAX_BYTES)
|
|
77
|
-
return undefined;
|
|
78
|
-
return text;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The append blocks for a session: the built-in rules, then the user's file.
|
|
86
|
-
*
|
|
87
|
-
* The file gets a heading naming its origin, because a rule whose source is
|
|
88
|
-
* invisible is a rule nobody can find again.
|
|
89
|
-
*
|
|
90
|
-
* `interactive` decides whether the first block also states that nobody can
|
|
91
|
-
* answer a question. The bundled skills include one that resolves a mode by
|
|
92
|
-
* asking the user and waiting, which is right in a terminal and fatal in
|
|
93
|
-
* `--print`: the model asks, nothing answers, and the task it was given never
|
|
94
|
-
* runs. The prompt said nothing either way, so the model could not tell the two
|
|
95
|
-
* apart. Pass the real value; a wrong one is worse than none.
|
|
96
|
-
*/
|
|
97
|
-
export function promptAppendBlocks(builtIn, interactive) {
|
|
98
|
-
const blocks = [interactive ? builtIn : `${builtIn}\n\n${NON_INTERACTIVE_BLOCK}`];
|
|
99
|
-
const user = readPromptFile(USER_PROMPT_FILE);
|
|
100
|
-
if (user)
|
|
101
|
-
blocks.push(`# Standing instructions from ${USER_PROMPT_FILE}\n${user}`);
|
|
102
|
-
return blocks;
|
|
103
|
-
}
|
package/dist/prompt-text.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read and write the text of a message whose content may be a string or a parts
|
|
3
|
-
* array.
|
|
4
|
-
*
|
|
5
|
-
* The prompt has to be edited in place on the leading system message: a provider
|
|
6
|
-
* reads the prompt and the initial tool declarations from that one message, so
|
|
7
|
-
* appending a second system message would not reach it. Both prompt extensions
|
|
8
|
-
* need the same two operations, and doing it in one place keeps the
|
|
9
|
-
* `unknown`-typed SDK content out of their bodies.
|
|
10
|
-
*/
|
|
11
|
-
/** Flatten a message's content to plain text. */
|
|
12
|
-
export function readMessageText(content) {
|
|
13
|
-
if (typeof content === "string")
|
|
14
|
-
return content;
|
|
15
|
-
if (Array.isArray(content)) {
|
|
16
|
-
return content
|
|
17
|
-
.map((part) => {
|
|
18
|
-
const candidate = part;
|
|
19
|
-
return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
|
|
20
|
-
})
|
|
21
|
-
.join("\n");
|
|
22
|
-
}
|
|
23
|
-
return content == null ? "" : String(content);
|
|
24
|
-
}
|
|
25
|
-
/** Write text back into a message's content, preserving its original shape. */
|
|
26
|
-
export function writeMessageText(content, text) {
|
|
27
|
-
if (Array.isArray(content)) {
|
|
28
|
-
const parts = content.map((part) => ({ ...part }));
|
|
29
|
-
const texts = parts.filter((part) => part.type === "text");
|
|
30
|
-
if (texts.length > 0) {
|
|
31
|
-
// Collapse into the first text part; any others would otherwise duplicate
|
|
32
|
-
// or contradict the text we just wrote.
|
|
33
|
-
texts[0].text = text;
|
|
34
|
-
for (const extra of texts.slice(1))
|
|
35
|
-
extra.text = "";
|
|
36
|
-
return parts;
|
|
37
|
-
}
|
|
38
|
-
return [...parts, { type: "text", text }];
|
|
39
|
-
}
|
|
40
|
-
return text;
|
|
41
|
-
}
|
package/dist/proxy.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Routing the CLI's own requests through a proxy.
|
|
3
|
-
*
|
|
4
|
-
* HowToFix talks to one upstream, `api.howtofix.id`, and that is the traffic
|
|
5
|
-
* this affects: the provider calls, the sign-in exchange, the release check. A
|
|
6
|
-
* user behind a corporate gateway, or on a network where that host is reachable
|
|
7
|
-
* only through one, otherwise cannot use the CLI at all.
|
|
8
|
-
*
|
|
9
|
-
* Two sources:
|
|
10
|
-
*
|
|
11
|
-
* 1. `httpsProxy` in `<agent dir>/settings.json` — what `/proxy` writes, and
|
|
12
|
-
* the only one that applies to this CLI alone. Set, it wins outright.
|
|
13
|
-
* 2. `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`, with `NO_PROXY` honoured —
|
|
14
|
-
* the conventional variables, read exactly the way Node reads them.
|
|
15
|
-
*
|
|
16
|
-
* The request is routed with an undici dispatcher rather than by replacing
|
|
17
|
-
* `fetch`, because the SDK's provider adapters call `globalThis.fetch` and
|
|
18
|
-
* swapping that out would be a monkey-patch of a dependency's dependency. The
|
|
19
|
-
* dispatcher is what Node uses underneath, so the behaviour is the platform's,
|
|
20
|
-
* not an imitation of it.
|
|
21
|
-
*/
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
import { EnvHttpProxyAgent, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
|
25
|
-
/** Where the setting lives, and what it is called. */
|
|
26
|
-
export const PROXY_SETTING_KEY = "httpsProxy";
|
|
27
|
-
/** The proxy named in settings.json, if there is a usable one there. */
|
|
28
|
-
export function configuredProxy(agentDir) {
|
|
29
|
-
let parsed;
|
|
30
|
-
try {
|
|
31
|
-
parsed = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8"));
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// A missing or unparseable settings file is "no proxy configured": routing
|
|
35
|
-
// must never be the thing that stops the CLI from starting.
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
const value = parsed[PROXY_SETTING_KEY];
|
|
39
|
-
if (typeof value !== "string")
|
|
40
|
-
return undefined;
|
|
41
|
-
const trimmed = value.trim();
|
|
42
|
-
// `/proxy off` clears the key; an empty string means the same thing.
|
|
43
|
-
return trimmed === "" ? undefined : trimmed;
|
|
44
|
-
}
|
|
45
|
-
/** Whether the conventional environment variables name a proxy. */
|
|
46
|
-
export function environmentProxy() {
|
|
47
|
-
const value = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.ALL_PROXY;
|
|
48
|
-
const trimmed = value?.trim();
|
|
49
|
-
return trimmed ? trimmed : undefined;
|
|
50
|
-
}
|
|
51
|
-
/** A URL this CLI can actually dial, or the reason it cannot. */
|
|
52
|
-
export function validateProxyUrl(raw) {
|
|
53
|
-
const value = raw.trim();
|
|
54
|
-
if (value === "")
|
|
55
|
-
return { ok: false, problem: "empty" };
|
|
56
|
-
let parsed;
|
|
57
|
-
try {
|
|
58
|
-
parsed = new URL(value);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return { ok: false, problem: `"${value}" is not a URL` };
|
|
62
|
-
}
|
|
63
|
-
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
64
|
-
// Only what the runtime can dial without another dependency: a typo is refused
|
|
65
|
-
// where it is typed, not at the next request.
|
|
66
|
-
if (scheme !== "http" && scheme !== "https") {
|
|
67
|
-
return { ok: false, problem: `unsupported scheme "${scheme}"; use http:// or https://` };
|
|
68
|
-
}
|
|
69
|
-
if (!parsed.hostname)
|
|
70
|
-
return { ok: false, problem: "no host in the proxy URL" };
|
|
71
|
-
return { ok: true, url: value };
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* The dispatcher this process started with, captured before the first override.
|
|
75
|
-
*
|
|
76
|
-
* Kept so "no proxy" can restore the exact object the runtime began with rather
|
|
77
|
-
* than a new one that merely behaves the same. Captured lazily, on the first
|
|
78
|
-
* call, which is always before any override — so it is the pristine default.
|
|
79
|
-
*/
|
|
80
|
-
let defaultDispatcher;
|
|
81
|
-
/**
|
|
82
|
-
* Route this process's outbound requests through the configured proxy.
|
|
83
|
-
*
|
|
84
|
-
* Called once, before any session exists, and again whenever the setting
|
|
85
|
-
* changes. Returns what it did, so the caller can say it in one line — a proxy
|
|
86
|
-
* that is silently in force is a support question waiting to happen.
|
|
87
|
-
*
|
|
88
|
-
* Every path sets a dispatcher, including the "none" one, and that is the point:
|
|
89
|
-
* a function that only ever installs a proxy cannot undo one. Turning the proxy
|
|
90
|
-
* off used to write the setting and leave the dispatcher in place, so the rest
|
|
91
|
-
* of that session kept dialling a host the user had just removed — the turn hung
|
|
92
|
-
* with nothing on screen to say why, and only a restart fixed it.
|
|
93
|
-
*
|
|
94
|
-
* A settings value is dialled directly, because the user named one host and
|
|
95
|
-
* meant it. Environment variables go through `EnvHttpProxyAgent`, which applies
|
|
96
|
-
* `NO_PROXY` and the lowercase spellings the way the platform does; matching
|
|
97
|
-
* those rules by hand is how the exceptions get subtly wrong.
|
|
98
|
-
*/
|
|
99
|
-
export function installProxy(agentDir) {
|
|
100
|
-
const restore = () => {
|
|
101
|
-
if (defaultDispatcher === undefined)
|
|
102
|
-
defaultDispatcher = getGlobalDispatcher();
|
|
103
|
-
setGlobalDispatcher(defaultDispatcher);
|
|
104
|
-
};
|
|
105
|
-
const fromSettings = configuredProxy(agentDir);
|
|
106
|
-
if (fromSettings) {
|
|
107
|
-
const valid = validateProxyUrl(fromSettings);
|
|
108
|
-
// A malformed value is reported through `source`, not thrown: the direct
|
|
109
|
-
// route is still worth trying, and a stack trace at startup is worse than
|
|
110
|
-
// one line naming the bad URL.
|
|
111
|
-
if (!valid.ok)
|
|
112
|
-
return { source: "settings" };
|
|
113
|
-
try {
|
|
114
|
-
setGlobalDispatcher(new ProxyAgent(valid.url));
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
restore();
|
|
118
|
-
return { source: "settings" };
|
|
119
|
-
}
|
|
120
|
-
return { url: valid.url, source: "settings" };
|
|
121
|
-
}
|
|
122
|
-
const fromEnv = environmentProxy();
|
|
123
|
-
if (fromEnv) {
|
|
124
|
-
try {
|
|
125
|
-
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
restore();
|
|
129
|
-
return { source: "env" };
|
|
130
|
-
}
|
|
131
|
-
return { url: fromEnv, source: "env" };
|
|
132
|
-
}
|
|
133
|
-
// No proxy anywhere: put the original dispatcher back, so a session that had
|
|
134
|
-
// one and then did not is genuinely direct rather than apparently direct.
|
|
135
|
-
restore();
|
|
136
|
-
return { source: "none" };
|
|
137
|
-
}
|
package/dist/quality/ast.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript AST wrapper: parse a set of files into walkable source files.
|
|
3
|
-
*
|
|
4
|
-
* Why `typescript/unstable/*`: TypeScript 7.0 ships NO stable programmatic API —
|
|
5
|
-
* the package's `.` export resolves to a version string, `createSourceFile` /
|
|
6
|
-
* `createProgram` / `forEachChild` are gone, and `unstable/ast` has no parser at
|
|
7
|
-
* all (scanner + node types only). Parsing lives in the native binary and is
|
|
8
|
-
* reachable only through the unstable session API, which spawns it over a pipe;
|
|
9
|
-
* a stable API is expected back in 7.1. `package.json` pins `typescript` to an
|
|
10
|
-
* exact version (no caret) precisely so a bump through `unstable/` is a reviewed
|
|
11
|
-
* upgrade, not a silent metric change. `scripts/check-quality.mjs` asserts both
|
|
12
|
-
* the pin and this comment.
|
|
13
|
-
*
|
|
14
|
-
* Two properties matter more than speed here (the spec's §4 regression):
|
|
15
|
-
*
|
|
16
|
-
* 1. Every requested file lands in the returned `Map<path, ParsedFile>` — a
|
|
17
|
-
* file that cannot be read or parsed becomes an explicit `{ ok: false }`
|
|
18
|
-
* entry, never a missing key. Analysis results are paired to files by path,
|
|
19
|
-
* never by array position, so one broken file cannot shift the rest.
|
|
20
|
-
* 2. Nested functions are separate units: the metric walkers stop at a
|
|
21
|
-
* function boundary and report the inner function on its own.
|
|
22
|
-
*/
|
|
23
|
-
import { API } from "typescript/unstable/async";
|
|
24
|
-
import { SyntaxKind, visitEachChild } from "typescript/unstable/ast";
|
|
25
|
-
/** Kinds that make a function its own analysis unit. */
|
|
26
|
-
export const FUNCTION_KINDS = new Set([SyntaxKind.FunctionDeclaration, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction, SyntaxKind.MethodDeclaration, SyntaxKind.Constructor, SyntaxKind.GetAccessor, SyntaxKind.SetAccessor]);
|
|
27
|
-
export function isFunctionLike(kind) {
|
|
28
|
-
return FUNCTION_KINDS.has(kind);
|
|
29
|
-
}
|
|
30
|
-
/** `&&`, `||`, `??` — one decision point each in cyclomatic, +1 flat in cognitive. */
|
|
31
|
-
export const LOGICAL_OPERATORS = new Set([SyntaxKind.AmpersandAmpersandToken, SyntaxKind.BarBarToken, SyntaxKind.QuestionQuestionToken]);
|
|
32
|
-
/**
|
|
33
|
-
* A `BinaryExpression` that tests with `&&` / `||` / `??`. TypeScript's AST has
|
|
34
|
-
* no `LogicalExpression` (that is an estree name); the operator is a child
|
|
35
|
-
* token, so it is looked up by name through {@link childNode}.
|
|
36
|
-
*/
|
|
37
|
-
export function isLogicalBinary(node) {
|
|
38
|
-
if (node.kind !== SyntaxKind.BinaryExpression)
|
|
39
|
-
return false;
|
|
40
|
-
const operator = childNode(node, "operatorToken");
|
|
41
|
-
return operator !== undefined && LOGICAL_OPERATORS.has(operator.kind);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Read a named child off a node.
|
|
45
|
-
*
|
|
46
|
-
* The generated node interfaces are per-kind, so generic code cannot touch
|
|
47
|
-
* `thenStatement` without a cast per kind; every metric child access goes
|
|
48
|
-
* through here instead, which also keeps `no any` intact.
|
|
49
|
-
*/
|
|
50
|
-
export function childNode(node, key) {
|
|
51
|
-
const value = node[key];
|
|
52
|
-
if (value && typeof value === "object" && typeof value.kind === "number") {
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
/** The node list stored under a key (`parameters`, `statements`, …), or `[]`. */
|
|
58
|
-
export function childNodes(node, key) {
|
|
59
|
-
const value = node[key];
|
|
60
|
-
return Array.isArray(value) ? value : [];
|
|
61
|
-
}
|
|
62
|
-
/** Whether the node's modifier list contains the given modifier keyword kind. */
|
|
63
|
-
export function hasModifier(node, kind) {
|
|
64
|
-
return childNodes(node, "modifiers").some((modifier) => modifier.kind === kind);
|
|
65
|
-
}
|
|
66
|
-
/** The `.text` of an identifier-like node, or `undefined` when there is none. */
|
|
67
|
-
export function nodeText(node) {
|
|
68
|
-
if (!node)
|
|
69
|
-
return undefined;
|
|
70
|
-
const text = node["text"];
|
|
71
|
-
return typeof text === "string" ? text : undefined;
|
|
72
|
-
}
|
|
73
|
-
/** 1-based line of a position in the file. Three call sites share the +1. */
|
|
74
|
-
export function lineOf(sourceFile, position) {
|
|
75
|
-
return sourceFile.getLineAndCharacterOfPosition(position).line + 1;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Span of a node for reporting: `getStart` skips leading trivia, so a JSDoc
|
|
79
|
-
* block does not inflate the printed `L88-142` or the SARIF region, and every
|
|
80
|
-
* consumer (the function units and `report.ts`'s findings) computes the same
|
|
81
|
-
* four numbers in one place.
|
|
82
|
-
*/
|
|
83
|
-
export function nodeSpan(sourceFile, node) {
|
|
84
|
-
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
85
|
-
const end = sourceFile.getLineAndCharacterOfPosition(node.end);
|
|
86
|
-
return {
|
|
87
|
-
startLine: start.line + 1,
|
|
88
|
-
startColumn: start.character + 1,
|
|
89
|
-
endLine: end.line + 1,
|
|
90
|
-
endColumn: end.character + 1,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Depth-first walk over a node and all of its descendants, nested functions
|
|
95
|
-
* included — this is how a file's function units are collected, because every
|
|
96
|
-
* nested function is its own unit.
|
|
97
|
-
*/
|
|
98
|
-
export function walk(root, visit) {
|
|
99
|
-
visit(root);
|
|
100
|
-
visitEachChild(root, (child) => {
|
|
101
|
-
walk(child, visit);
|
|
102
|
-
return child;
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Walk a function's body for ITS metrics: the body node itself is visited,
|
|
107
|
-
* but a nested function child is not entered — it is a separate unit with its
|
|
108
|
-
* own report entry, and counting it twice would make every metric a lie.
|
|
109
|
-
*/
|
|
110
|
-
export function walkOwn(root, visit) {
|
|
111
|
-
visit(root);
|
|
112
|
-
visitEachChild(root, (child) => {
|
|
113
|
-
if (isFunctionLike(child.kind))
|
|
114
|
-
return child;
|
|
115
|
-
walkOwn(child, visit);
|
|
116
|
-
return child;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
const BREAK_AT_FUNCTION = FUNCTION_KINDS;
|
|
120
|
-
/**
|
|
121
|
-
* Name for a function-like node. Declarations and methods carry one directly;
|
|
122
|
-
* an arrow or function expression takes the name of the variable, property, or
|
|
123
|
-
* parameter it is being assigned to, climbing only up to the enclosing function
|
|
124
|
-
* so a `return () =>` does not borrow its parent's name.
|
|
125
|
-
*/
|
|
126
|
-
export function functionName(node) {
|
|
127
|
-
const own = nodeText(childNode(node, "name"));
|
|
128
|
-
if (own)
|
|
129
|
-
return own;
|
|
130
|
-
let parent = childNode(node, "parent");
|
|
131
|
-
while (parent && !BREAK_AT_FUNCTION.has(parent.kind)) {
|
|
132
|
-
if (parent.kind === SyntaxKind.VariableDeclaration || parent.kind === SyntaxKind.PropertyAssignment) {
|
|
133
|
-
const name = nodeText(childNode(parent, "name"));
|
|
134
|
-
if (name)
|
|
135
|
-
return name;
|
|
136
|
-
}
|
|
137
|
-
parent = childNode(parent, "parent");
|
|
138
|
-
}
|
|
139
|
-
return "<anonymous>";
|
|
140
|
-
}
|
|
141
|
-
function collectFunctions(sourceFile) {
|
|
142
|
-
const units = [];
|
|
143
|
-
walk(sourceFile, (node) => {
|
|
144
|
-
if (!isFunctionLike(node.kind))
|
|
145
|
-
return;
|
|
146
|
-
const span = nodeSpan(sourceFile, node);
|
|
147
|
-
units.push({ name: functionName(node), node, startLine: span.startLine, endLine: span.endLine });
|
|
148
|
-
});
|
|
149
|
-
return units;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Parse every file into the Map, keyed by the path it was asked for.
|
|
153
|
-
*
|
|
154
|
-
* A file that fails to read, resolves to no project, or has syntactic
|
|
155
|
-
* diagnostics becomes an error entry — explicit, attributed, and never
|
|
156
|
-
* dropped. One session serves the whole batch; an empty batch never spawns
|
|
157
|
-
* the native compiler at all (an empty directory is a valid analysis).
|
|
158
|
-
*/
|
|
159
|
-
export async function parseFiles(paths) {
|
|
160
|
-
const results = new Map();
|
|
161
|
-
if (paths.length === 0)
|
|
162
|
-
return results;
|
|
163
|
-
const api = new API({ cwd: process.cwd() });
|
|
164
|
-
try {
|
|
165
|
-
const snapshot = await api.updateSnapshot({ openFiles: [...paths] });
|
|
166
|
-
for (const path of paths) {
|
|
167
|
-
try {
|
|
168
|
-
const project = await snapshot.getDefaultProjectForFile(path);
|
|
169
|
-
if (!project) {
|
|
170
|
-
results.set(path, { ok: false, error: "no TypeScript project contains this file" });
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const sourceFile = await project.program.getSourceFile(path);
|
|
174
|
-
if (!sourceFile) {
|
|
175
|
-
results.set(path, { ok: false, error: "the compiler could not read this file" });
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const diagnostics = await project.program.getSyntacticDiagnostics(path);
|
|
179
|
-
if (diagnostics.length > 0) {
|
|
180
|
-
const first = diagnostics[0];
|
|
181
|
-
const where = lineOf(sourceFile, first.pos);
|
|
182
|
-
results.set(path, { ok: false, error: `line ${where}: ${first.text} (TS${first.code})` });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
results.set(path, { ok: true, sourceFile, functions: collectFunctions(sourceFile) });
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
results.set(path, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
await api.close();
|
|
194
|
-
}
|
|
195
|
-
return results;
|
|
196
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cognitive complexity (Sonar method, simplified) — spec §3.2.
|
|
3
|
-
*
|
|
4
|
-
* What separates this from cyclomatic, and why both exist:
|
|
5
|
-
*
|
|
6
|
-
* 1. **The nesting penalty.** A structural construct costs `1 + depth`, where
|
|
7
|
-
* depth is how many structural constructs enclose it. Three sibling `if`s
|
|
8
|
-
* cost 3; three nested `if`s cost 1 + 2 + 3 = 6. `scripts/check-quality.mjs`
|
|
9
|
-
* asserts both numbers — without the asymmetry this metric is just a second
|
|
10
|
-
* cyclomatic.
|
|
11
|
-
* 2. **Flat increments.** `else` / `else if` add 1 with no nesting penalty, and
|
|
12
|
-
* `&&` / `||` / `??` add 1 flat. A ternary adds nothing here (cyclomatic
|
|
13
|
-
* counts it instead).
|
|
14
|
-
*
|
|
15
|
-
* A chained `else if` is one flat `+1` for the chain link — it does not get a
|
|
16
|
-
* second structural count on top (the parent already paid the marker), and it
|
|
17
|
-
* does not deepen the block it guards: code under `if / else if / else` sits at
|
|
18
|
-
* the same level regardless of how many links the chain has.
|
|
19
|
-
*
|
|
20
|
-
* Nested functions are not entered: they are separate units, reported on
|
|
21
|
-
* their own.
|
|
22
|
-
*/
|
|
23
|
-
import { SyntaxKind, visitEachChild } from "typescript/unstable/ast";
|
|
24
|
-
import { childNode, isFunctionLike, isLogicalBinary } from "./ast.js";
|
|
25
|
-
/** Nodes that add `1 + depth` and deepen their children. `try` is deliberately absent (spec §3.2). */
|
|
26
|
-
const STRUCTURAL_KINDS = new Set([
|
|
27
|
-
SyntaxKind.IfStatement,
|
|
28
|
-
SyntaxKind.SwitchStatement,
|
|
29
|
-
SyntaxKind.ForStatement,
|
|
30
|
-
SyntaxKind.ForInStatement,
|
|
31
|
-
SyntaxKind.ForOfStatement,
|
|
32
|
-
SyntaxKind.WhileStatement,
|
|
33
|
-
SyntaxKind.DoStatement,
|
|
34
|
-
SyntaxKind.CatchClause,
|
|
35
|
-
]);
|
|
36
|
-
export function cognitive(body) {
|
|
37
|
-
let score = 0;
|
|
38
|
-
const enterNode = (node, depth) => {
|
|
39
|
-
if (isFunctionLike(node.kind))
|
|
40
|
-
return;
|
|
41
|
-
if (node.kind === SyntaxKind.IfStatement) {
|
|
42
|
-
enterIf(node, depth, false);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (STRUCTURAL_KINDS.has(node.kind)) {
|
|
46
|
-
score += 1 + depth;
|
|
47
|
-
visitEachChild(node, (child) => {
|
|
48
|
-
enterNode(child, depth + 1);
|
|
49
|
-
return child;
|
|
50
|
-
});
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
// `&&` / `||` / `??` add 1 flat; everything else descends unchanged.
|
|
54
|
-
if (isLogicalBinary(node))
|
|
55
|
-
score += 1;
|
|
56
|
-
visitEachChild(node, (child) => {
|
|
57
|
-
enterNode(child, depth);
|
|
58
|
-
return child;
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
const enterIf = (node, depth, chained) => {
|
|
62
|
-
// A chained `else if` was already counted as its parent's flat marker.
|
|
63
|
-
if (!chained)
|
|
64
|
-
score += 1 + depth;
|
|
65
|
-
const condition = childNode(node, "expression");
|
|
66
|
-
if (condition)
|
|
67
|
-
enterNode(condition, depth);
|
|
68
|
-
const then = childNode(node, "thenStatement");
|
|
69
|
-
if (then)
|
|
70
|
-
enterNode(then, depth + 1);
|
|
71
|
-
const alternate = childNode(node, "alternate");
|
|
72
|
-
if (!alternate)
|
|
73
|
-
return;
|
|
74
|
-
// `else` and `else if` both add exactly 1, flat (spec §3.2).
|
|
75
|
-
score += 1;
|
|
76
|
-
if (alternate.kind === SyntaxKind.IfStatement)
|
|
77
|
-
enterIf(alternate, depth, true);
|
|
78
|
-
else
|
|
79
|
-
enterNode(alternate, depth + 1);
|
|
80
|
-
};
|
|
81
|
-
enterNode(body, 0);
|
|
82
|
-
return score;
|
|
83
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cyclomatic complexity (McCabe) — spec §3.1.
|
|
3
|
-
*
|
|
4
|
-
* `1 + number of decision points`. A decision point is an `if`, a ternary, a
|
|
5
|
-
* `&&` / `||` / `??` test, a `case` (never `default`, which tests nothing), a
|
|
6
|
-
* loop of any shape, or a `catch`. The walk starts at the function body and
|
|
7
|
-
* stops at nested function boundaries: those are their own units and would
|
|
8
|
-
* otherwise be counted twice.
|
|
9
|
-
*/
|
|
10
|
-
import { SyntaxKind } from "typescript/unstable/ast";
|
|
11
|
-
import { isLogicalBinary, walkOwn } from "./ast.js";
|
|
12
|
-
const DECISION_KINDS = new Set([
|
|
13
|
-
SyntaxKind.IfStatement,
|
|
14
|
-
SyntaxKind.ConditionalExpression,
|
|
15
|
-
SyntaxKind.WhileStatement,
|
|
16
|
-
SyntaxKind.DoStatement,
|
|
17
|
-
SyntaxKind.ForStatement,
|
|
18
|
-
SyntaxKind.ForInStatement,
|
|
19
|
-
SyntaxKind.ForOfStatement,
|
|
20
|
-
SyntaxKind.CatchClause,
|
|
21
|
-
SyntaxKind.CaseClause, // DefaultClause is a different kind and is not counted
|
|
22
|
-
]);
|
|
23
|
-
export function cyclomatic(body) {
|
|
24
|
-
let complexity = 1;
|
|
25
|
-
walkOwn(body, (node) => {
|
|
26
|
-
if (DECISION_KINDS.has(node.kind) || isLogicalBinary(node))
|
|
27
|
-
complexity += 1;
|
|
28
|
-
});
|
|
29
|
-
return complexity;
|
|
30
|
-
}
|
package/dist/quality/halstead.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Halstead volume — spec §3.5.
|
|
3
|
-
*
|
|
4
|
-
* `V = N × log2(n)` where `n` is the number of distinct operators and
|
|
5
|
-
* operands and `N` their total occurrences. Volume feeds the Maintainability
|
|
6
|
-
* Index and stands alone as a signal: high volume with low branching is a long
|
|
7
|
-
* but straight function — a helper-extraction candidate.
|
|
8
|
-
*
|
|
9
|
-
* The walk starts at the function body and stops at nested function
|
|
10
|
-
* boundaries, so an inner function's vocabulary is not charged to its parent.
|
|
11
|
-
* An empty body has no vocabulary at all: volume is defined as 0 there, never
|
|
12
|
-
* `log2(0)` — the degenerate-input case `check-quality` pins.
|
|
13
|
-
*/
|
|
14
|
-
import { SyntaxKind } from "typescript/unstable/ast";
|
|
15
|
-
import { childNode, nodeText, walkOwn } from "./ast.js";
|
|
16
|
-
/**
|
|
17
|
-
* Kinds Halstead counts as operators. Mapped from the reference implementation
|
|
18
|
-
* (estree names) onto TypeScript's AST: estree's `MemberExpression` is
|
|
19
|
-
* `PropertyAccessExpression` + `ElementAccessExpression` here, and its
|
|
20
|
-
* `LogicalExpression` / `AssignmentExpression` are both `BinaryExpression`,
|
|
21
|
-
* distinguished by the operator token in the key.
|
|
22
|
-
*/
|
|
23
|
-
const OPERATOR_KINDS = new Set([
|
|
24
|
-
SyntaxKind.BinaryExpression,
|
|
25
|
-
SyntaxKind.PrefixUnaryExpression,
|
|
26
|
-
SyntaxKind.PostfixUnaryExpression,
|
|
27
|
-
SyntaxKind.ConditionalExpression,
|
|
28
|
-
SyntaxKind.CallExpression,
|
|
29
|
-
SyntaxKind.NewExpression,
|
|
30
|
-
SyntaxKind.PropertyAccessExpression,
|
|
31
|
-
SyntaxKind.ElementAccessExpression,
|
|
32
|
-
SyntaxKind.AwaitExpression,
|
|
33
|
-
SyntaxKind.YieldExpression,
|
|
34
|
-
SyntaxKind.SpreadElement,
|
|
35
|
-
SyntaxKind.IfStatement,
|
|
36
|
-
SyntaxKind.WhileStatement,
|
|
37
|
-
SyntaxKind.DoStatement,
|
|
38
|
-
SyntaxKind.ForStatement,
|
|
39
|
-
SyntaxKind.ForInStatement,
|
|
40
|
-
SyntaxKind.ForOfStatement,
|
|
41
|
-
SyntaxKind.SwitchStatement,
|
|
42
|
-
SyntaxKind.ReturnStatement,
|
|
43
|
-
SyntaxKind.ThrowStatement,
|
|
44
|
-
SyntaxKind.TryStatement,
|
|
45
|
-
SyntaxKind.BreakStatement,
|
|
46
|
-
SyntaxKind.ContinueStatement,
|
|
47
|
-
]);
|
|
48
|
-
/**
|
|
49
|
-
* Literal kinds counted as operands. `true` / `false` / `null` are keyword
|
|
50
|
-
* tokens in this AST and carry no stored text, so they are deliberately not in
|
|
51
|
-
* the set rather than listed and silently skipped.
|
|
52
|
-
*/
|
|
53
|
-
const LITERAL_KINDS = new Set([SyntaxKind.StringLiteral, SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.RegularExpressionLiteral, SyntaxKind.NoSubstitutionTemplateLiteral]);
|
|
54
|
-
function count(into, key) {
|
|
55
|
-
into.set(key, (into.get(key) ?? 0) + 1);
|
|
56
|
-
}
|
|
57
|
-
export function halsteadVolume(body) {
|
|
58
|
-
const operators = new Map();
|
|
59
|
-
const operands = new Map();
|
|
60
|
-
walkOwn(body, (node) => {
|
|
61
|
-
if (OPERATOR_KINDS.has(node.kind)) {
|
|
62
|
-
const operator = childNode(node, "operatorToken");
|
|
63
|
-
count(operators, SyntaxKind[node.kind] + (operator ? `:${SyntaxKind[operator.kind]}` : ""));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const text = nodeText(node);
|
|
67
|
-
if (text === undefined)
|
|
68
|
-
return;
|
|
69
|
-
if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier)
|
|
70
|
-
count(operands, `id:${text}`);
|
|
71
|
-
else if (LITERAL_KINDS.has(node.kind))
|
|
72
|
-
count(operands, `lit:${text}`);
|
|
73
|
-
});
|
|
74
|
-
const vocabulary = operators.size + operands.size;
|
|
75
|
-
if (vocabulary === 0)
|
|
76
|
-
return 0;
|
|
77
|
-
let total = 0;
|
|
78
|
-
for (const seen of operators.values())
|
|
79
|
-
total += seen;
|
|
80
|
-
for (const seen of operands.values())
|
|
81
|
-
total += seen;
|
|
82
|
-
return Math.round(total * Math.log2(vocabulary) * 100) / 100;
|
|
83
|
-
}
|