akm-cli 0.9.7 → 0.9.8-beta.2
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 +310 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/data-dir-usage.js +165 -0
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +18 -37
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/commands/workflow-cli.js +11 -61
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-io.js +21 -8
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +74 -2
- package/dist/core/state-db.js +2 -1
- package/dist/core/warn.js +15 -0
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +43 -22
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/dir-staleness.js +30 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +179 -248
- package/dist/scripts/akm-migrate.js +179 -248
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +24 -28
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +16 -1
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +15 -0
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/task-to-v3.js +10 -2
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
- package/docs/reference/cli.md +3 -1
- package/docs/reference/data-and-telemetry.md +1 -0
- package/docs/reference/tasks.md +11 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -9
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -11,14 +11,13 @@ import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
|
11
11
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
12
12
|
import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
|
|
13
13
|
import { deriveBundleIds } from "../../core/bundle-id.js";
|
|
14
|
-
import { isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
|
|
14
|
+
import { compareCodePoints, isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
|
|
15
15
|
import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
|
|
16
16
|
import { UsageError } from "../../core/errors.js";
|
|
17
17
|
import { warn } from "../../core/warn.js";
|
|
18
18
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
19
|
import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail } from "../../tasks/source-v3.js";
|
|
20
20
|
import { resolveWorkflowSourceDomains } from "../../workflows/source-files.js";
|
|
21
|
-
import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/compare.js";
|
|
22
21
|
import { runBaseChecks } from "./base-linter.js";
|
|
23
22
|
import { checkEnvForDangerousKeys } from "./env-key-rules.js";
|
|
24
23
|
import { isAdvisoryLintIssue } from "./types.js";
|
|
@@ -87,7 +86,7 @@ function collectWorkflowFiles(dir) {
|
|
|
87
86
|
const results = [];
|
|
88
87
|
for (const entry of fs
|
|
89
88
|
.readdirSync(dir, { withFileTypes: true })
|
|
90
|
-
.sort((left, right) =>
|
|
89
|
+
.sort((left, right) => compareCodePoints(left.name, right.name))) {
|
|
91
90
|
if (entry.name === ".git")
|
|
92
91
|
continue;
|
|
93
92
|
const full = path.join(dir, entry.name);
|
|
@@ -103,7 +102,7 @@ function collectWorkflowFiles(dir) {
|
|
|
103
102
|
if (extension === ".md" || extension === ".yml")
|
|
104
103
|
results.push(full);
|
|
105
104
|
}
|
|
106
|
-
return results.sort(
|
|
105
|
+
return results.sort(compareCodePoints);
|
|
107
106
|
}
|
|
108
107
|
/**
|
|
109
108
|
* Resolve the already-enumerated candidates through the same workflow-source
|
|
@@ -131,7 +130,7 @@ function resolveWorkflowLintOwnership(stashRoot, files) {
|
|
|
131
130
|
fixed: false,
|
|
132
131
|
});
|
|
133
132
|
}
|
|
134
|
-
return { files: ownedFiles.sort(
|
|
133
|
+
return { files: ownedFiles.sort(compareCodePoints), issues };
|
|
135
134
|
}
|
|
136
135
|
// ── Non-akm adapter dispatch (real `adapter.validate()`, not a re-implementation) ──
|
|
137
136
|
//
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Legacy `extraParams` -> first-class-field lift (#852, following #815), as
|
|
6
|
+
* an `akm migrate` concern.
|
|
7
|
+
*
|
|
8
|
+
* This used to run silently, in memory, on every config load
|
|
9
|
+
* (`liftLegacyEngineExtraParams` called from `parseAndValidateConfigText`)
|
|
10
|
+
* and never wrote the result back — so the lift, and its warning, recurred
|
|
11
|
+
* forever. config.json is akm-owned plain JSON; the lift is deterministic
|
|
12
|
+
* and total, so — like the dead-residue cleanup in `./dead-residue.ts` —
|
|
13
|
+
* it belongs in `akm migrate`, run once, persisted. `parseAndValidateConfigText`
|
|
14
|
+
* now fails closed on an unmigrated config instead of lifting it.
|
|
15
|
+
*/
|
|
16
|
+
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, writeConfigAtomic, } from "../../core/config/config-io.js";
|
|
17
|
+
import { liftLegacyEngineExtraParams } from "../../core/extra-params.js";
|
|
18
|
+
function readRawConfig(configPath) {
|
|
19
|
+
const text = readConfigText(configPath);
|
|
20
|
+
if (text === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return parseConfigText(text, configPath);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read-only: what `akm migrate apply` would lift (or flag as conflicting) in
|
|
26
|
+
* `config.json`'s `extraParams`. Never touches disk. Returns an empty plan
|
|
27
|
+
* when the config file does not exist.
|
|
28
|
+
*/
|
|
29
|
+
export function findConfigExtraParamsLift(configPath) {
|
|
30
|
+
const raw = readRawConfig(configPath);
|
|
31
|
+
if (!raw)
|
|
32
|
+
return { lifted: [], conflicts: [] };
|
|
33
|
+
const { lifted, conflicts } = liftLegacyEngineExtraParams(raw);
|
|
34
|
+
return { lifted, conflicts };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Persist the legacy extraParams -> first-class field lift to `config.json`,
|
|
38
|
+
* once. Best-effort like `removeDeadResidue`: a genuine conflict (extraParams
|
|
39
|
+
* and the first-class field set to different values) is left untouched here
|
|
40
|
+
* — `parseAndValidateConfigText` already hard-rejects that config at every
|
|
41
|
+
* load with the exact mismatch, so a second, weaker error here would only be
|
|
42
|
+
* noise — and is reported back via `conflicts` instead.
|
|
43
|
+
*/
|
|
44
|
+
export function applyConfigExtraParamsLift(configPath) {
|
|
45
|
+
const raw = readRawConfig(configPath);
|
|
46
|
+
if (!raw)
|
|
47
|
+
return { applied: false, lifted: [], conflicts: [] };
|
|
48
|
+
const { config, lifted, conflicts } = liftLegacyEngineExtraParams(raw);
|
|
49
|
+
if (conflicts.length > 0 || lifted.length === 0) {
|
|
50
|
+
return { applied: false, lifted, conflicts };
|
|
51
|
+
}
|
|
52
|
+
const release = acquireConfigLock();
|
|
53
|
+
try {
|
|
54
|
+
backupExistingConfig(configPath);
|
|
55
|
+
writeConfigAtomic(configPath, config);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
release();
|
|
59
|
+
}
|
|
60
|
+
return { applied: true, lifted, conflicts: [] };
|
|
61
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead pre-0.9.0 `$STASH/.akm` residue: superseded filesystem layouts with no
|
|
3
|
+
* live reader or writer anywhere in src/ (each verified by grep before being
|
|
4
|
+
* listed — see #889 for the audit). The largest, `.akm/proposals/`, was
|
|
5
|
+
* replaced by the `proposals` table in state.db in 0.9.0 and measured 135 MB
|
|
6
|
+
* on a real bundle.
|
|
7
|
+
*
|
|
8
|
+
* This lives under `migrate/` because removing a superseded layout IS
|
|
9
|
+
* migration — the tail end of the moves that created these paths' replacements.
|
|
10
|
+
* It was first shipped as a bolted-on `akm health --clean-dead-residue` flag
|
|
11
|
+
* plus a health advisory; that was the wrong shape (a special-purpose switch
|
|
12
|
+
* apologizing for migrations that did not finish their own job) and was
|
|
13
|
+
* removed. `akm migrate status` reports what is here; `akm migrate apply`
|
|
14
|
+
* removes it, exactly as it applies every other pending migration.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
const DEAD_RESIDUE_PATHS = [
|
|
19
|
+
{ name: "proposals", reason: "superseded by the `proposals` table in $DATA/state.db (0.9.0)" },
|
|
20
|
+
{ prefix: "runs.archived-", reason: "orphaned archive of a directory that no longer exists" },
|
|
21
|
+
{
|
|
22
|
+
name: "archive",
|
|
23
|
+
reason: "legacy consolidation archive; current prune writes .akm/memory-cleanup/archive/ instead",
|
|
24
|
+
},
|
|
25
|
+
{ name: "graph.json", reason: "superseded by the graph_* tables in $DATA/index.db" },
|
|
26
|
+
{ name: "consolidate-journal.json", reason: "legacy consolidation journal; unused" },
|
|
27
|
+
{ name: "proposals.db", reason: "empty legacy database file" },
|
|
28
|
+
{ name: "mv-transactions", reason: "legacy fs-txn journal location; unused" },
|
|
29
|
+
];
|
|
30
|
+
function dirSizeBytes(target) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const entryPath = path.join(target, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
total += dirSizeBytes(entryPath);
|
|
43
|
+
}
|
|
44
|
+
else if (entry.isFile()) {
|
|
45
|
+
try {
|
|
46
|
+
total += fs.statSync(entryPath).size;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Skip files that vanish or are inaccessible between readdir and stat.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return total;
|
|
54
|
+
}
|
|
55
|
+
function sizeOf(target) {
|
|
56
|
+
const st = fs.statSync(target);
|
|
57
|
+
return st.isDirectory() ? dirSizeBytes(target) : st.size;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find every Tier-1 dead-residue path that actually exists under
|
|
61
|
+
* `$STASH/.akm`, with its computed size. Read-only — never deletes.
|
|
62
|
+
*/
|
|
63
|
+
export function findDeadResidueEntries(stashDir) {
|
|
64
|
+
const akmDir = path.join(stashDir, ".akm");
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = fs.readdirSync(akmDir);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const found = [];
|
|
73
|
+
for (const spec of DEAD_RESIDUE_PATHS) {
|
|
74
|
+
const matches = spec.name !== undefined ? [spec.name] : names.filter((n) => n.startsWith(spec.prefix ?? "\0"));
|
|
75
|
+
for (const name of matches) {
|
|
76
|
+
if (!names.includes(name))
|
|
77
|
+
continue;
|
|
78
|
+
const absolutePath = path.join(akmDir, name);
|
|
79
|
+
let sizeBytes;
|
|
80
|
+
try {
|
|
81
|
+
sizeBytes = sizeOf(absolutePath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue; // vanished between readdir and stat
|
|
85
|
+
}
|
|
86
|
+
found.push({ relativePath: path.join(".akm", name), absolutePath, sizeBytes, reason: spec.reason });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delete every Tier-1 dead-residue path found under `$STASH/.akm`. Only
|
|
93
|
+
* invoked when the caller has explicitly opted in (`akm health
|
|
94
|
+
* --clean-dead-residue`) — never as a side effect of a plain `akm health`
|
|
95
|
+
* read. Best-effort per-path: one failure does not abort the rest.
|
|
96
|
+
*/
|
|
97
|
+
export function removeDeadResidue(stashDir) {
|
|
98
|
+
const entries = findDeadResidueEntries(stashDir);
|
|
99
|
+
return entries.map((entry) => {
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(entry.absolutePath, { recursive: true, force: true });
|
|
102
|
+
return { relativePath: entry.relativePath, sizeBytes: entry.sizeBytes, removed: true };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
relativePath: entry.relativePath,
|
|
107
|
+
sizeBytes: entry.sizeBytes,
|
|
108
|
+
removed: false,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Stale durable-transaction journals (`$DATA/txn/<rootNs24>/<id>/journal.json`,
|
|
6
|
+
* see `core/fs-txn.ts`) that never got recovered — a crash mid-transaction
|
|
7
|
+
* left a journal on disk with no process left to finish or roll it back.
|
|
8
|
+
* `akm health` used to only report these and point at a troubleshooting doc
|
|
9
|
+
* (issue: an advisory that tells the user to go read docs instead of the
|
|
10
|
+
* tool recovering its own interrupted state). Recovery IS a migration
|
|
11
|
+
* concern like dead `.akm/` residue: `akm migrate status` names what is
|
|
12
|
+
* here for the stash root, `akm migrate apply` recovers it, exactly like
|
|
13
|
+
* `dead-residue.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Recovery goes through {@link recoverTxnsForRoot}, which requires every
|
|
16
|
+
* live kind's registrar to be IMPORTED first so its handler is registered
|
|
17
|
+
* (see fs-txn.ts's module docs). `proposal/repository.ts` registers the
|
|
18
|
+
* `proposal`/`proposal-reject` kinds specifically so "ANY recovery entry
|
|
19
|
+
* point ... can finish or roll back an interrupted proposal mutation for a
|
|
20
|
+
* root it touches" (its own comment on the registration) — this module is
|
|
21
|
+
* exactly that kind of entry point.
|
|
22
|
+
*/
|
|
23
|
+
import { canonicalTxnRoot, listTxnJournalsTolerant, recoverTxnsForRoot } from "../../core/fs-txn.js";
|
|
24
|
+
// Side-effect import: registers the `proposal`/`proposal-reject` txn kinds
|
|
25
|
+
// so recovery below can roll them forward/back for the stash root.
|
|
26
|
+
import "../proposal/repository.js";
|
|
27
|
+
/**
|
|
28
|
+
* Find every durable-transaction journal bound to `stashDir`'s namespace.
|
|
29
|
+
* Read-only, tolerant of a corrupt journal (counted, not thrown on) — mirrors
|
|
30
|
+
* `findDeadResidueEntries`'s read-only/never-mutates contract.
|
|
31
|
+
*/
|
|
32
|
+
export function findStaleTxnEntries(stashDir) {
|
|
33
|
+
const root = canonicalTxnRoot(stashDir);
|
|
34
|
+
const { matches } = listTxnJournalsTolerant((j) => canonicalTxnRoot(j.root) === root);
|
|
35
|
+
return matches.map(({ journal }) => journalToEntry(journal));
|
|
36
|
+
}
|
|
37
|
+
function journalToEntry(journal) {
|
|
38
|
+
return { transactionId: journal.transactionId, kind: journal.kind, phase: journal.phase, root: journal.root };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recover every durable transaction bound to `stashDir`'s namespace: roll
|
|
42
|
+
* back journals before their kind's commit point, roll forward the rest.
|
|
43
|
+
* The counterpart to {@link findStaleTxnEntries}, invoked only from `akm
|
|
44
|
+
* migrate apply`.
|
|
45
|
+
*/
|
|
46
|
+
export async function recoverStaleTxns(stashDir) {
|
|
47
|
+
const recovered = await recoverTxnsForRoot(stashDir);
|
|
48
|
+
return recovered.map(journalToEntry);
|
|
49
|
+
}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
5
|
+
import { resolveStashDir } from "../core/common.js";
|
|
6
|
+
import { ConfigError } from "../core/errors.js";
|
|
7
|
+
import { getConfigPath } from "../core/paths.js";
|
|
8
|
+
import { applyConfigExtraParamsLift, findConfigExtraParamsLift } from "./migrate/config-extra-params.js";
|
|
9
|
+
import { findDeadResidueEntries, removeDeadResidue } from "./migrate/dead-residue.js";
|
|
10
|
+
import { findStaleTxnEntries, recoverStaleTxns } from "./migrate/stale-txn.js";
|
|
5
11
|
import { runMigrationTool } from "./migration-tool.js";
|
|
6
12
|
async function callMigrateTool(args, runTool) {
|
|
7
13
|
const result = await runTool(args);
|
|
@@ -79,6 +85,25 @@ export function resolveGenerationStatus(call, label) {
|
|
|
79
85
|
* got to look at a stable tree in that case.
|
|
80
86
|
*/
|
|
81
87
|
export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runTool = runMigrationTool) {
|
|
88
|
+
// Superseded pre-0.9.0 .akm layouts are a migration concern like any other:
|
|
89
|
+
// status names them, apply removes them. (First shipped as a bolted-on
|
|
90
|
+
// `health --clean-dead-residue` flag; folded here where it belongs.) The
|
|
91
|
+
// legacy extraParams -> first-class-field config lift (#852) is the same
|
|
92
|
+
// shape: status names it, apply persists it once instead of the old
|
|
93
|
+
// permanent silent lift on every config load.
|
|
94
|
+
// No configured bundle means there is no stash to scan — an empty domain,
|
|
95
|
+
// not an error — so migrate still works before `akm bundle create`. Any
|
|
96
|
+
// OTHER ConfigError propagates.
|
|
97
|
+
let stashDir;
|
|
98
|
+
try {
|
|
99
|
+
stashDir = resolveStashDir();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(error instanceof ConfigError) || error.code !== "STASH_DIR_NOT_FOUND")
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const configPath = getConfigPath();
|
|
106
|
+
const applyResidue = command === "migrate-apply" && !genOneArgs.includes("--dry-run");
|
|
82
107
|
const first = await callMigrateTool(genOneArgs, runTool);
|
|
83
108
|
if (first.status !== EXIT_CODES.SUCCESS && first.status !== EXIT_CODES.GENERAL) {
|
|
84
109
|
process.exitCode = first.status;
|
|
@@ -95,7 +120,23 @@ export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runT
|
|
|
95
120
|
return;
|
|
96
121
|
}
|
|
97
122
|
const combined = combineMigrationPlans(first, second);
|
|
98
|
-
|
|
123
|
+
// The stash-scoped sections need a bundle to scan; no configured bundle is
|
|
124
|
+
// an empty domain, not an error, so migrate still works before
|
|
125
|
+
// `akm bundle create`. The config lift is config-scoped and always runs.
|
|
126
|
+
const stashSections = stashDir === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
deadResidue: applyResidue
|
|
130
|
+
? { removed: removeDeadResidue(stashDir) }
|
|
131
|
+
: { pending: findDeadResidueEntries(stashDir) },
|
|
132
|
+
staleTxns: applyResidue
|
|
133
|
+
? { recovered: await recoverStaleTxns(stashDir) }
|
|
134
|
+
: { pending: findStaleTxnEntries(stashDir) },
|
|
135
|
+
};
|
|
136
|
+
const configExtraParams = applyResidue
|
|
137
|
+
? applyConfigExtraParamsLift(configPath)
|
|
138
|
+
: { pending: findConfigExtraParamsLift(configPath) };
|
|
139
|
+
output(command, { ...combined, ...stashSections, configExtraParams });
|
|
99
140
|
if (combined.status === "blocked")
|
|
100
141
|
process.exitCode = EXIT_CODES.GENERAL;
|
|
101
142
|
}
|
|
@@ -15,7 +15,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
15
15
|
import { loadConfig } from "../../core/config/config.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
18
|
-
import {
|
|
18
|
+
import { diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, recoverProposalTransactionsForStash, rejectProposalDurably, resolveProposalId, revertProposal, } from "./repository.js";
|
|
19
19
|
import { validateProposal } from "./validators/proposals.js";
|
|
20
20
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
21
21
|
function resolveStash(stashDir) {
|
|
@@ -131,26 +131,6 @@ export function akmProposalDiff(options) {
|
|
|
131
131
|
...(diff.targetPath ? { targetPath: diff.targetPath } : {}),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
export function akmProposalCreate(options) {
|
|
135
|
-
const queue = resolveProposalQueue(options.stashDir, options.queue, options.config);
|
|
136
|
-
const stash = queue.stashDir;
|
|
137
|
-
const target = options.target ?? (queue.target ? { source: queue.target.source.name, root: queue.target.source.path } : undefined);
|
|
138
|
-
// Manual proposal creation (via `akm proposal create`) always bypasses
|
|
139
|
-
// dedup/cooldown guards — the operator is explicitly requesting a proposal.
|
|
140
|
-
const result = createProposal(stash, {
|
|
141
|
-
ref: options.ref,
|
|
142
|
-
source: options.source,
|
|
143
|
-
...(target !== undefined ? { target } : {}),
|
|
144
|
-
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
145
|
-
payload: options.payload,
|
|
146
|
-
force: true,
|
|
147
|
-
}, options.ctx);
|
|
148
|
-
if (isProposalSkipped(result)) {
|
|
149
|
-
// Should never happen with force:true — defensive only.
|
|
150
|
-
throw new Error(`Unexpected proposal skip: ${result.message}`);
|
|
151
|
-
}
|
|
152
|
-
return { schemaVersion: 1, ok: true, proposal: result };
|
|
153
|
-
}
|
|
154
134
|
/**
|
|
155
135
|
* Restore an accepted proposal's prior content from the backup captured at
|
|
156
136
|
* promotion time (Advantage D6c / Phase 6C).
|
|
@@ -563,9 +563,6 @@ export function listProposalsReadOnly(stashDir, options = {}, ctx) {
|
|
|
563
563
|
}
|
|
564
564
|
});
|
|
565
565
|
}
|
|
566
|
-
catch {
|
|
567
|
-
return [];
|
|
568
|
-
}
|
|
569
566
|
finally {
|
|
570
567
|
db?.close();
|
|
571
568
|
}
|
|
@@ -1013,7 +1010,6 @@ async function finalizeProposalTransaction(txn, target, proposal, ctx) {
|
|
|
1013
1010
|
paths: [commitPath],
|
|
1014
1011
|
snapshots: p.gitSnapshots ?? {},
|
|
1015
1012
|
onCommitRecorded: (commit) => {
|
|
1016
|
-
// biome-ignore lint/style/noNonNullAssertion: publishWriteTargetTransaction throws when absent
|
|
1017
1013
|
const publication = p.gitPublication;
|
|
1018
1014
|
if (publication.commit !== commit) {
|
|
1019
1015
|
publication.commit = commit;
|
|
@@ -190,12 +190,6 @@ export function hasHotCaptureMode(frontmatter) {
|
|
|
190
190
|
return frontmatter?.captureMode === "hot";
|
|
191
191
|
}
|
|
192
192
|
// ── Consolidate merge size gate ──────────────────────────────────────────────
|
|
193
|
-
/**
|
|
194
|
-
* Ratio lower-bound for merged body vs. the larger source body.
|
|
195
|
-
* Lower than reflect (0.5) because deduplication is expected — two memories
|
|
196
|
-
* with 80-90% overlap legitimately compress to well under 50% of the larger.
|
|
197
|
-
*/
|
|
198
|
-
export const MERGE_SHRINK_RATIO_MIN = 0.3;
|
|
199
193
|
// ── Reflect size gate ────────────────────────────────────────────────────────
|
|
200
194
|
/** Ratio lower-bound: proposed body must be at least this fraction of source. */
|
|
201
195
|
export const REFLECT_SHRINK_RATIO_MIN = 0.5;
|
|
@@ -52,7 +52,9 @@ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
|
52
52
|
export const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
53
53
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
54
54
|
const CURATE_CLOSE_SCORE_BAND = 0.12;
|
|
55
|
-
|
|
55
|
+
// Used by `shouldRunCurateFallback` to judge whether the initial search
|
|
56
|
+
// already returned strong results (a different purpose from ranking/keeping
|
|
57
|
+
// hits — see that function for its own rationale).
|
|
56
58
|
const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
|
|
57
59
|
const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
|
|
58
60
|
const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
|
|
@@ -577,11 +579,6 @@ function compareCurateHits(a, b) {
|
|
|
577
579
|
return rawDiff;
|
|
578
580
|
return a.originalIndex - b.originalIndex;
|
|
579
581
|
}
|
|
580
|
-
function passesCurateScoreFloor(hit, leaderScore) {
|
|
581
|
-
if (leaderScore === undefined)
|
|
582
|
-
return true;
|
|
583
|
-
return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
584
|
-
}
|
|
585
582
|
function isNarrowReferenceFamilyQuery(query, family) {
|
|
586
583
|
if (!family || family.role !== "reference")
|
|
587
584
|
return false;
|
|
@@ -602,19 +599,8 @@ function selectCuratedStashHits(query, hits, limit) {
|
|
|
602
599
|
const ranked = collapsed.hits
|
|
603
600
|
.map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
|
|
604
601
|
.sort(compareCurateHits);
|
|
605
|
-
const selected = [];
|
|
606
602
|
const supportRefsByRef = collapsed.supportRefsByRef;
|
|
607
|
-
|
|
608
|
-
for (const candidate of ranked) {
|
|
609
|
-
if (!passesCurateScoreFloor(candidate, leaderScore))
|
|
610
|
-
continue;
|
|
611
|
-
selected.push(candidate);
|
|
612
|
-
if (leaderScore === undefined)
|
|
613
|
-
leaderScore = candidate.rawScore;
|
|
614
|
-
if (selected.length >= limit)
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
|
|
603
|
+
return { selected: ranked.slice(0, limit).map((entry) => entry.hit), supportRefsByRef };
|
|
618
604
|
}
|
|
619
605
|
function collapseCurateFamilies(query, hits) {
|
|
620
606
|
const passthrough = [];
|
|
@@ -27,7 +27,7 @@ import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.
|
|
|
27
27
|
import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
|
|
28
28
|
import { asNonEmptyString, isWithin } from "../../core/common.js";
|
|
29
29
|
import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
|
|
30
|
-
import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
30
|
+
import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
31
31
|
import { appendEvent } from "../../core/events.js";
|
|
32
32
|
import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
|
|
33
33
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
@@ -292,6 +292,7 @@ export async function showLocal(input) {
|
|
|
292
292
|
}
|
|
293
293
|
catch (err) {
|
|
294
294
|
rethrowIfTestIsolationError(err);
|
|
295
|
+
rethrowIfDataDirUnreadable(err);
|
|
295
296
|
return { total: 0, hits: [] };
|
|
296
297
|
}
|
|
297
298
|
})(),
|
|
@@ -47,7 +47,11 @@ function canonicalJson(value) {
|
|
|
47
47
|
if (value && typeof value === "object") {
|
|
48
48
|
const entries = Object.entries(value)
|
|
49
49
|
.filter(([, entry]) => entry !== undefined)
|
|
50
|
-
|
|
50
|
+
// Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
|
|
51
|
+
// bundle-audit generation, and localeCompare is ICU/locale-dependent — the
|
|
52
|
+
// same object could hash differently on two machines. Matches the other
|
|
53
|
+
// canonicalJson implementations (workflows/ir/plan-hash.ts).
|
|
54
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
51
55
|
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
52
56
|
}
|
|
53
57
|
return JSON.stringify(value);
|
|
@@ -333,7 +333,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
333
333
|
ok: false,
|
|
334
334
|
skipped: opts.skip,
|
|
335
335
|
message: `Upgrade completed, but the state schema was not prepared (${detail}). ` +
|
|
336
|
-
"Preserve state.db and run `akm upgrade --
|
|
336
|
+
"Preserve state.db and run `akm upgrade --state-only` before other AKM commands (the binary is already current).",
|
|
337
337
|
};
|
|
338
338
|
}
|
|
339
339
|
const stateNote = stateUpgrade.safetyCopyPath ? ` Historical state safety copy: ${stateUpgrade.safetyCopyPath}.` : "";
|
|
@@ -520,3 +520,40 @@ export function getPackageManagerUpgradeCommand(installMethod, packageName = get
|
|
|
520
520
|
}
|
|
521
521
|
return undefined;
|
|
522
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Apply pending historical destructive state.db migrations WITHOUT installing a
|
|
525
|
+
* new akm — the body of `akm upgrade --state-only`.
|
|
526
|
+
*
|
|
527
|
+
* Migrations flagged `historical-destructive` are refused during an ordinary
|
|
528
|
+
* managed open: they need a verified sibling safety copy taken under the
|
|
529
|
+
* migration writer lock, and that is deliberate, so an unattended `akm index`
|
|
530
|
+
* can never quietly drop operator state.
|
|
531
|
+
*
|
|
532
|
+
* The bug this fixes is not the guard but its reachability (#895). The only
|
|
533
|
+
* code path that set `allowHistoricalDestructiveStateUpgrade` ran as a
|
|
534
|
+
* POST-INSTALL step of a real upgrade, so it sat behind an npm install. Where
|
|
535
|
+
* akm is installed globally by an image and the runtime user is unprivileged,
|
|
536
|
+
* that install fails EACCES and throws long before the migration is reached —
|
|
537
|
+
* leaving the documented remedy impossible to run and `akm index --full`
|
|
538
|
+
* permanently blocked. Nothing about the migration itself needs the network,
|
|
539
|
+
* root, or a new binary; it is local, offline, and already verified.
|
|
540
|
+
*
|
|
541
|
+
* The safety copy is NOT skipped here. This changes only who may ask for the
|
|
542
|
+
* migration, never what it does.
|
|
543
|
+
*/
|
|
544
|
+
export function upgradeStateOnly(currentVersion, dependencies) {
|
|
545
|
+
const upgradeState = dependencies?.upgradeHistoricalStateDatabase ?? upgradeHistoricalStateDatabase;
|
|
546
|
+
const result = upgradeState();
|
|
547
|
+
return {
|
|
548
|
+
currentVersion,
|
|
549
|
+
newVersion: currentVersion,
|
|
550
|
+
upgraded: false,
|
|
551
|
+
installMethod: detectInstallMethod(),
|
|
552
|
+
message: result.upgraded
|
|
553
|
+
? `Applied pending state.db migrations. Safety copy: ${result.safetyCopyPath}`
|
|
554
|
+
: "state.db is already current; no migration was needed",
|
|
555
|
+
stateUpgrade: result.safetyCopyPath
|
|
556
|
+
? { applied: result.upgraded, safetyCopyPath: result.safetyCopyPath }
|
|
557
|
+
: { applied: result.upgraded },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
@@ -33,7 +33,7 @@ import { UsageError } from "../../core/errors.js";
|
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
34
34
|
import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
|
|
35
35
|
import { pkgVersion } from "../../version.js";
|
|
36
|
-
import { checkForUpdate, performUpgrade } from "./self-update.js";
|
|
36
|
+
import { checkForUpdate, performUpgrade, upgradeStateOnly } from "./self-update.js";
|
|
37
37
|
import { akmClone } from "./source-clone.js";
|
|
38
38
|
export const upgradeCommand = defineJsonCommand({
|
|
39
39
|
meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
|
|
@@ -45,8 +45,24 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
45
45
|
description: "Skip the post-upgrade index rebuild",
|
|
46
46
|
default: false,
|
|
47
47
|
},
|
|
48
|
+
"state-only": {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
description: "Apply pending state.db migrations without installing a new akm",
|
|
51
|
+
default: false,
|
|
52
|
+
},
|
|
48
53
|
},
|
|
49
54
|
async run({ args }) {
|
|
55
|
+
// Applying a historical destructive state migration used to be reachable
|
|
56
|
+
// ONLY as a post-install step of a real upgrade, so an install akm cannot
|
|
57
|
+
// rewrite -- a global npm install owned by root, an image that ships the
|
|
58
|
+
// CLI -- had no route to it at all: the npm step fails EACCES and throws
|
|
59
|
+
// long before the migration runs (#895). The migration is a local,
|
|
60
|
+
// offline, already-verified operation; it does not need the network or a
|
|
61
|
+
// new binary, and coupling it to one was the bug.
|
|
62
|
+
if (args["state-only"]) {
|
|
63
|
+
output("upgrade", upgradeStateOnly(pkgVersion));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
50
66
|
const check = await checkForUpdate(pkgVersion);
|
|
51
67
|
if (args.check) {
|
|
52
68
|
output("upgrade", check);
|
|
@@ -875,14 +875,6 @@ function warnIneligibleRebind(runtime, explicitRebind, warnings) {
|
|
|
875
875
|
return;
|
|
876
876
|
warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
877
877
|
}
|
|
878
|
-
export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
|
|
879
|
-
const invocation = (deps.resolveInvocation ?? resolveAkmInvocation)();
|
|
880
|
-
if (!invocation.eligible && !explicitRebind) {
|
|
881
|
-
throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
|
|
882
|
-
}
|
|
883
|
-
const contextPath = (deps.writeDescriptor ?? writeSchedulerContextDescriptor)(schedulerContextDescriptor());
|
|
884
|
-
return { binding: invocation.argv, contextPath, eligible: invocation.eligible, kind: invocation.kind };
|
|
885
|
-
}
|
|
886
878
|
function groupInstalledBindings(entries, invocation) {
|
|
887
879
|
const groups = new Map();
|
|
888
880
|
for (const entry of entries) {
|
|
@@ -2,40 +2,32 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Check every URL in `entries` and report the ones that are dead.
|
|
7
|
+
*
|
|
8
|
+
* No cap, no per-entry slice, no ceiling option. There used to be a
|
|
9
|
+
* `MAX_URLS = 20` plus an undocumented `slice(0, 3)` per entry, so this
|
|
10
|
+
* examined at most twenty links in a bundle holding thousands and reported
|
|
11
|
+
* success. The first attempt at fixing that kept the cap and layered a
|
|
12
|
+
* coverage report, a warning, and an override on top — a constraint plus more
|
|
13
|
+
* code to explain the constraint. Both are gone. It checks what you asked it
|
|
14
|
+
* to check.
|
|
15
|
+
*
|
|
16
|
+
* A request that fails, times out, or cannot resolve surfaces as a `DeadUrl`
|
|
17
|
+
* rather than being swallowed, so a network problem is visible instead of
|
|
18
|
+
* looking like a clean bill of health.
|
|
19
|
+
*/
|
|
7
20
|
export async function checkDeadUrls(_stashDir, entries) {
|
|
8
|
-
const urlsToCheck = [];
|
|
9
|
-
for (const entry of entries) {
|
|
10
|
-
if (urlsToCheck.length >= MAX_URLS)
|
|
11
|
-
break;
|
|
12
|
-
const matches = entry.body.match(URL_RE) ?? [];
|
|
13
|
-
for (const url of matches.slice(0, 3)) {
|
|
14
|
-
urlsToCheck.push({ ref: entry.ref, url });
|
|
15
|
-
if (urlsToCheck.length >= MAX_URLS)
|
|
16
|
-
break;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
21
|
+
const urlsToCheck = entries.flatMap((entry) => (entry.body.match(URL_RE) ?? []).map((url) => ({ ref: entry.ref, url })));
|
|
19
22
|
const results = [];
|
|
20
23
|
await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
|
|
21
24
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
method: "HEAD",
|
|
26
|
-
signal: controller.signal,
|
|
27
|
-
redirect: "follow",
|
|
28
|
-
});
|
|
29
|
-
clearTimeout(timer);
|
|
30
|
-
if (res.status >= 400) {
|
|
25
|
+
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
26
|
+
if (res.status >= 400)
|
|
31
27
|
results.push({ ref, url, status: res.status });
|
|
32
|
-
}
|
|
33
28
|
}
|
|
34
|
-
catch
|
|
35
|
-
|
|
36
|
-
results.push({ ref, url, status: "timeout" });
|
|
37
|
-
}
|
|
38
|
-
// network errors (ENOTFOUND etc.) — skip, don't report as dead
|
|
29
|
+
catch {
|
|
30
|
+
results.push({ ref, url, status: "error" });
|
|
39
31
|
}
|
|
40
32
|
}));
|
|
41
33
|
return results;
|