ast-mcp-server 0.13.1 → 0.14.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 +26 -4
- package/README.md +24 -10
- package/cordis.patch.yml +1 -1
- package/dist/batch/schema.js +1 -1
- package/dist/services/authenticated-publication.js +181 -0
- package/dist/services/call-spines.js +10 -5
- package/dist/services/context-builder.js +2 -1
- package/dist/services/diagnostics.js +293 -9
- package/dist/services/impact.js +46 -14
- package/dist/services/managed-file.js +3 -50
- package/dist/services/operation-plan-file.js +17 -18
- package/dist/services/operations.js +231 -87
- package/dist/services/relationships.js +403 -4
- package/dist/services/test-candidates.js +35 -2
- package/dist/tools/find_test_candidates.js +24 -3
- package/dist/tools/get_impact.js +12 -2
- package/dist/tools/relationship-schema.js +12 -1
- package/docs/support.md +4 -2
- package/package.json +9 -7
- package/skills/structural-code-editing/SKILL.md +8 -0
- package/skills/structural-code-editing/releases.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,18 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.
|
|
9
|
+
## [0.14.1] — not yet published (2026-10-26)
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- Deduplicated `zod` in the lockfile to the patched `4.6.7` (GHSA-mrhx-c7xc-m3w5). The `@modelcontextprotocol/sdk` range `^3.25 || ^4.0` had kept a second, vulnerable `4.4.3` copy beside the direct dependency; `yarn audit` did not report it. No runtime code or advertised tool contract changes.
|
|
14
|
+
|
|
15
|
+
## [0.14.0] — published to `latest` and `next`, registry verified (2026-10-25)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Migrated schema validation to Zod 4 (`^4.6.7`). The published `tools/list` contract changes shape and Zod's validation error messages differ: tool input schemas no longer all declare `additionalProperties: false` (16 of 16 before, 1 of 16 now), shared `$ref` definitions are inlined (27 before, 0 now), the serialized `tools/list` grows from 100,923 to 126,562 bytes, integer fields declare a maximum of 9007199254740991, and UUID fields gain a `pattern`. Clients that cache or diff the advertised schemas should refresh them.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- Upgraded `@modelcontextprotocol/sdk` to `^1.32.1` (GHSA-6qxp-vccf-f47h), `body-parser` to 2.4.0, and `zod` to 4.6.7 for the memory-exhaustion advisory, and raised the `hono` and `fast-uri` resolutions to 4.13.13 and 3.1.9. `yarn audit` reports no suggestions.
|
|
24
|
+
|
|
25
|
+
### Release status
|
|
26
|
+
|
|
27
|
+
- npm published immutable `0.14.0` bytes with integrity `sha512-+g1GpmB9ZMd4tB/z7o75AZ4LdnqEn+MQDiRh0LDBV2j4xb9iKB8XMoiME47tW0u2HgfxWsKtQBChGxfnrz3dVw==`. Public-registry verification passed; `latest` and `next` both resolve to `0.14.0`; and npm `gitHead`, the annotated `v0.14.0` tag, and the GitHub Release target exact commit `f6e64751398f69eb3f0f1abc808f4221f7d2bc52`.
|
|
28
|
+
- Publish run `40232707904` reported its readback as ambiguous because of registry lag although npm had accepted the version; verification `40249874010` and promotion `40255528749` are green and authoritative.
|
|
29
|
+
|
|
30
|
+
## [0.13.1] — published to `latest` and `next`, registry verified (2026-09-01)
|
|
10
31
|
|
|
11
32
|
### Fixed
|
|
12
33
|
|
|
13
|
-
- The DeepSeek Harness adapter now opts into a bounded canonical-JSON text projection when a successful MCP result has structured data but no ordinary text, preserving lossless `structuredContent` while making native Agent/Session results useful and durable. The mandatory pinned-host smoke freezes the public 0.13.0 RED baseline and verifies the
|
|
14
|
-
- `ast_explore` now publishes its complete object input schema while retaining all cross-field runtime rejection, and `ast_get_impact` no longer advertises one false output shape across JSON and TOON. The pinned Harness gate binds the public empty-schema RED to
|
|
34
|
+
- The DeepSeek Harness adapter now opts into a bounded canonical-JSON text projection when a successful MCP result has structured data but no ordinary text, preserving lossless `structuredContent` while making native Agent/Session results useful and durable. The mandatory pinned-host smoke freezes the public 0.13.0 RED baseline and verifies the released correction across the next model request, persisted `tool/result`, cold replay, guarded catalog, and cleanup readback.
|
|
35
|
+
- `ast_explore` now publishes its complete object input schema while retaining all cross-field runtime rejection, and `ast_get_impact` no longer advertises one false output shape across JSON and TOON. The pinned Harness gate binds the public empty-schema RED to released registry/native parity and unchanged hashes for the other 14 model schemas.
|
|
15
36
|
- Timeout ownership is now explicit across the Harness, queue, and AST worker: the shipped outer timeout exceeds the complete queue/execution/margin budget, deterministic slow work returns bounded AST-owned operational errors, queued work never starts after its deadline, cancellation remains correlated across worker generations, and cleanup proves zero active work, held requests, listeners, or owned processes.
|
|
16
37
|
- Harness lifecycle handling now converges deterministically through MCP removal and reconnect, rejects retired-generation effects, preserves AST `REQUEST_CANCELLED` authority separately from bridge transport observations, and completes correlated shutdown without residual state. The pinned native and rendered-GUI gates verify the `15→0→15` catalog sequence, request/header identity, secret-safe diagnostics, and ordered cleanup that still runs every owner check after an earlier cleanup failure.
|
|
17
38
|
|
|
18
39
|
### Release status
|
|
19
40
|
|
|
20
|
-
-
|
|
41
|
+
- npm published immutable `0.13.1` bytes with integrity `sha512-jqgGoYs8fe7J+E25lZusLK4wV6sjM5n5qiWnfe1RJIxOFo1r5nbtcBr1a/fdSWTYf/37bUNkshQp86UrdBHOsA==`. Public-registry verification passed; `latest` and `next` both resolve to `0.13.1`; and npm `gitHead`, the annotated `v0.13.1` tag, and the GitHub Release target exact commit `27b80a3da169b473a3b5c5dfea69ed52903ed4c7`.
|
|
42
|
+
- Main CI `33460585683`, Security `33460585625`, publish `33461292810`, verification `33461385440`, and promotion retry `33461651288` are green. Initial promotion run `33461571363` changed `latest` successfully but its bounded readback timed out; the verified idempotent retry is authoritative.
|
|
21
43
|
|
|
22
44
|
## [0.13.0] — published to `latest` and `next`, registry verified (2026-08-29)
|
|
23
45
|
|
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ Reads can start with a bounded file slice, a compact outline, or exact source on
|
|
|
46
46
|
- Use `ast_explore` when the question spans discovery and evidence. Its default summary is bounded; use `detail: "context"` for selected source and `detail: "full"` for source plus compiler references.
|
|
47
47
|
- Use `ast_get_outline` for a compact body-free view of a known file without source lines.
|
|
48
48
|
- Use `ast_get_symbol_source` when one declaration or implementation is the required evidence.
|
|
49
|
-
- Use `ast_get_impact` when the exact symbol is known and bounded direct/transitive compiler relationships are needed
|
|
50
|
-
- Use `ast_find_test_candidates` when an exact symbol should map to conservative
|
|
49
|
+
- Use `ast_get_impact` when the exact symbol is known and bounded direct/transitive compiler relationships are needed. Check `coverage`, `work`, `truncation`, `incomplete`, and `proven_empty`; this is read-only evidence, not a mutation plan.
|
|
50
|
+
- Use `ast_find_test_candidates` when an exact symbol should map to conservative tests. It admits only complete incoming `reference`, `import`, `export`, `extends`, `implements`, and `call` evidence, returns whole paths, and never executes tests.
|
|
51
51
|
|
|
52
52
|
`snapshot_state: "fresh"` means that the returned file bytes match the synchronized compiler snapshot. The separate `freshness` object describes the project/session state and preserves causes such as source changes or watcher failure. Neither field means that the project has zero TypeScript diagnostics; use `ast_get_diagnostics` for compiler errors and warnings.
|
|
53
53
|
|
|
@@ -64,7 +64,20 @@ The server exposes evidence labels instead of collapsing every result into an un
|
|
|
64
64
|
|
|
65
65
|
Freshness is orthogonal to TypeScript diagnostics. `fresh` means the evidence matches the synchronized snapshot; `pending`, `rebuilding`, `stale`, or `degraded` means the response must not be presented as current compiler evidence. Read tools expose the state, causes (`source_change`, `config_change`, `index_failure`, `watcher_failure`, or `compiler_rebuild`), and bounded `checked_at` timestamp. `ast_get_impact` refuses non-fresh compiler relationships. `ast_explore` returns the state together with `completeness`, `unresolved`, `budget`, and `truncation` metadata rather than silently dropping evidence.
|
|
66
66
|
|
|
67
|
-
All reads are budgeted. Callers control pagination and, where applicable, `max_bytes`, `reference_limit`, `max_depth`, `max_nodes`, and `max_edges`; responses report
|
|
67
|
+
All reads are budgeted. Callers control pagination and, where applicable, `max_bytes`, `reference_limit`, `max_depth`, `max_nodes`, and `max_edges`; responses report effective limits. A bounded stop is incomplete evidence, not an empty negative result.
|
|
68
|
+
|
|
69
|
+
Impact has two independent completeness channels:
|
|
70
|
+
|
|
71
|
+
| Channel | Evidence | Incomplete when |
|
|
72
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
73
|
+
| Semantic coverage | One canonical cell per requested kind, direction, and endpoint class: `not_applicable`, `completed`, `unsupported`, or `unfinished` | Any applicable cell is `unsupported` or `unfinished` |
|
|
74
|
+
| Traversal/work | `truncation` plus `work.work_limit_reached` and its counters | A depth/node/edge/work bound prevents completion |
|
|
75
|
+
|
|
76
|
+
`incomplete` is true when either channel is incomplete. `proven_empty` is true only when there are zero edges, every applicable cell completed, and no bound was exhausted. The public seven-kind/default impact request includes `contains`; because no scoped `contains` producer exists, applicable default or explicit containment is `unsupported`, so the result is incomplete even when traversal was not truncated.
|
|
77
|
+
|
|
78
|
+
`ast_find_test_candidates` deliberately excludes `contains` and fixes the admission scope to six incoming kinds: `reference`, `import`, `export`, `extends`, `implements`, and `call`. It returns `INCOMPLETE_EVIDENCE` before pagination for stale, inexact, unresolved, truncated, work-exhausted, unsupported, or unfinished evidence. Only complete six-kind authority may return `candidates: []` with `completeness.proven_empty: true`; pagination slices deterministic candidates while each proof and the unpaginated `coverage`, `work`, and traversal metadata stay whole.
|
|
79
|
+
|
|
80
|
+
These fields are additive: existing edge shapes, kind strings, defaults, and public errors remain stable. MCP `ast_get_impact` JSON and TOON represent the same logical coverage/work result; candidate MCP output remains canonical JSON, while read-only batch output may encode that same candidate result as TOON. Property, element, dynamic, computed-key, and external-alternative dispatch remains edge-free and unfinished—this contract does not certify the deferred #219 or #220 classifiers.
|
|
68
81
|
|
|
69
82
|
## Why this helps
|
|
70
83
|
|
|
@@ -84,7 +97,7 @@ The included batch benchmark records a 50% reduction in model round-trips and a
|
|
|
84
97
|
|
|
85
98
|
## Supported environment and trust boundary
|
|
86
99
|
|
|
87
|
-
Published v0.12.0 requires Node.js `>=22.13.0`; its immutable evidence matrix targets exact Node.js 22.13.0 and the current Node.js 24 line.
|
|
100
|
+
Published v0.12.0 requires Node.js `>=22.13.0`; its immutable evidence matrix targets exact Node.js 22.13.0 and the current Node.js 24 line. Structural apply and managed setup-file publication are verified only on Linux x64 with GNU coreutils 9.7 `mv` supporting `--update=none-fail`, `--exchange`, `--no-copy`, and `--no-target-directory`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, `O_DIRECTORY`/`O_NOFOLLOW`, and a destination filesystem that passes the owned link/exchange identity probe. A failed or denied primitive blocks mutation before source effects; there is no rename, copy/delete, or pathname-only fallback. Other Linux architectures or systems without this complete matrix, macOS, and Windows remain unverified.
|
|
88
101
|
|
|
89
102
|
This is a local stdio server. It runs with the invoking user's filesystem permissions, and clients may request any `project_root` that user can access. It does not provide HTTP authentication, sandboxing, tenant isolation, or a remote-service security boundary. Remote, untrusted, and multi-tenant operation is unsupported.
|
|
90
103
|
|
|
@@ -340,7 +353,7 @@ For a local candidate, pack and install its tarball instead:
|
|
|
340
353
|
|
|
341
354
|
```bash
|
|
342
355
|
yarn pack --out ast-mcp-server-%v.tgz
|
|
343
|
-
dsh plugin --profile web add ./ast-mcp-server-0.
|
|
356
|
+
dsh plugin --profile web add ./ast-mcp-server-0.14.1.tgz
|
|
344
357
|
```
|
|
345
358
|
|
|
346
359
|
The first supported surface is **reads + prepare + preview**. Every apply path is
|
|
@@ -529,12 +542,13 @@ Apply loads the exact retained postimages, requires the separately supplied revi
|
|
|
529
542
|
|
|
530
543
|
### Guarantee boundary
|
|
531
544
|
|
|
532
|
-
The server does **not** claim a filesystem-wide transaction:
|
|
545
|
+
The server does **not** claim a filesystem-wide transaction or safety against a continuously mutating writer:
|
|
533
546
|
|
|
534
|
-
-
|
|
535
|
-
-
|
|
536
|
-
-
|
|
537
|
-
- MCP and CLI apply share a fail-closed filesystem lock keyed by canonical `tsconfig.json` when they use the same state directory. It does not coordinate editors, NFS writers, or hostile external processes.
|
|
547
|
+
- Each replacement exchanges and authenticates one staged/destination pair. Multi-file apply remains sequential, so earlier postimages may be visible before later files commit.
|
|
548
|
+
- Reverse rollback restores only an exact operation-owned pair. Lost ownership preserves the observable entries and returns `AMBIGUOUS_APPLY`; it never falls back to pathname replacement.
|
|
549
|
+
- Creation is no-clobber. Once a created inode is published, a later failure cannot safely infer unlink ownership, so the destination and hidden stage are preserved and rollback fails closed as ambiguous.
|
|
550
|
+
- MCP and CLI apply share a fail-closed filesystem lock keyed by canonical `tsconfig.json` when they use the same state directory. It does not coordinate editors, NFS writers, writers using another configuration, or hostile external processes.
|
|
551
|
+
- Deterministic promise barriers prove the tested publication/rollback interleavings. They are a threat-boundary test seam, not a global atomicity or continuous-writer guarantee.
|
|
538
552
|
- Receipt persistence runs before that lock is released. If receipt storage fails after source replacement, apply exits non-zero and reports that verified postimages may be present; retry recovers the receipt only when the complete workspace exactly matches the reviewed post-workspace fingerprint.
|
|
539
553
|
- A hard process crash can leave a stale lock. Remove it only after inspecting its metadata and proving no apply is running; exact complete postimages can then recover the receipt, while partial or divergent state remains a conflict.
|
|
540
554
|
- Source encoding support is UTF-8, with or without BOM. Unsupported encodings are rejected.
|
package/cordis.patch.yml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Mounts the packaged `ast-mcp-server` stdio command through the official
|
|
4
4
|
# `@deepseek-ai/dsh-mcp-client` bridge. Harness does not resolve MCP binaries
|
|
5
5
|
# from PATH, so the entrypoint is resolved relative to the profile directory
|
|
6
|
-
# (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.
|
|
6
|
+
# (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.14.1.tgz`
|
|
7
7
|
# installs a locally packed package as a dependency.
|
|
8
8
|
#
|
|
9
9
|
# First-slice surface: reads + prepare + preview. Every apply path is denied
|
package/dist/batch/schema.js
CHANGED
|
@@ -20,7 +20,7 @@ const BatchStepSchema = z
|
|
|
20
20
|
.object({
|
|
21
21
|
id: z.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/),
|
|
22
22
|
tool: BatchToolSchema,
|
|
23
|
-
input: z.record(z.unknown()).default({}),
|
|
23
|
+
input: z.record(z.string(), z.unknown()).default({}),
|
|
24
24
|
foreach: z
|
|
25
25
|
.object({ $ref: z.string().min(1) })
|
|
26
26
|
.strict()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
function sameStat(left, right) {
|
|
7
|
+
const fields = ["dev", "ino", "size", "mode", "mtimeNs", "ctimeNs"];
|
|
8
|
+
return fields.every((field) => left[field] === right[field]);
|
|
9
|
+
}
|
|
10
|
+
export async function snapshotHeldFile(handle) {
|
|
11
|
+
const before = await handle.stat({ bigint: true });
|
|
12
|
+
if (!before.isFile() || before.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
13
|
+
throw new Error("Publication descriptor cannot be authenticated");
|
|
14
|
+
const bytes = await readFile(`/proc/self/fd/${handle.fd}`);
|
|
15
|
+
const after = await handle.stat({ bigint: true });
|
|
16
|
+
if (!after.isFile() || !sameStat(before, after))
|
|
17
|
+
throw new Error("Publication descriptor changed during read");
|
|
18
|
+
return {
|
|
19
|
+
dev: after.dev.toString(),
|
|
20
|
+
ino: after.ino.toString(),
|
|
21
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
22
|
+
mode: Number(after.mode & 511n),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function equal(left, right) {
|
|
26
|
+
return (left !== undefined &&
|
|
27
|
+
right !== undefined &&
|
|
28
|
+
left.dev === right.dev &&
|
|
29
|
+
left.ino === right.ino &&
|
|
30
|
+
left.sha256 === right.sha256 &&
|
|
31
|
+
left.mode === right.mode);
|
|
32
|
+
}
|
|
33
|
+
async function entry(directory, basename) {
|
|
34
|
+
const target = `/proc/self/fd/${directory.fd}/${basename}`;
|
|
35
|
+
let handle;
|
|
36
|
+
try {
|
|
37
|
+
handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
38
|
+
const snapshot = await snapshotHeldFile(handle);
|
|
39
|
+
const current = await lstat(target, { bigint: true });
|
|
40
|
+
return current.dev.toString() === snapshot.dev && current.ino.toString() === snapshot.ino
|
|
41
|
+
? snapshot
|
|
42
|
+
: undefined;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === "ENOENT")
|
|
46
|
+
return undefined;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await handle?.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function runPublicationPrimitive(executable, args, fds) {
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(executable, args, {
|
|
56
|
+
env: { LANG: "C", LC_ALL: "C", PATH: "/usr/bin:/bin" },
|
|
57
|
+
shell: false,
|
|
58
|
+
stdio: ["ignore", "ignore", "pipe", ...fds],
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
});
|
|
61
|
+
let failure;
|
|
62
|
+
let stderrBytes = 0;
|
|
63
|
+
const fail = (message, cause) => {
|
|
64
|
+
if (failure)
|
|
65
|
+
return;
|
|
66
|
+
failure = new Error(message, cause === undefined ? undefined : { cause });
|
|
67
|
+
child.kill("SIGKILL");
|
|
68
|
+
};
|
|
69
|
+
const timeout = setTimeout(() => fail("Publication primitive timed out"), 5_000);
|
|
70
|
+
child.stderr.on("data", (chunk) => {
|
|
71
|
+
stderrBytes += chunk.byteLength;
|
|
72
|
+
if (stderrBytes > 4_096)
|
|
73
|
+
fail("Publication primitive stderr exceeded limit");
|
|
74
|
+
});
|
|
75
|
+
child.on("error", (error) => fail("Publication primitive unavailable", error));
|
|
76
|
+
child.on("close", (code, signal) => {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
if (failure)
|
|
79
|
+
reject(failure);
|
|
80
|
+
else if (code === 0 && signal === null)
|
|
81
|
+
resolve();
|
|
82
|
+
else
|
|
83
|
+
reject(new Error(`Publication primitive failed (${signal ?? code ?? "unknown"})`));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export const exchangeDirectoryEntries = (directory, left, right) => runPublicationPrimitive("/usr/bin/mv", ["--exchange", "--no-copy", "-T", "--", `/proc/self/fd/3/${left}`, `/proc/self/fd/3/${right}`], [directory.fd]);
|
|
88
|
+
export const linkHeldFile = (source, directory, basename) => runPublicationPrimitive("/usr/bin/ln", ["-L", "-T", "--", "/proc/self/fd/3", `/proc/self/fd/4/${basename}`], [source.fd, directory.fd]);
|
|
89
|
+
export async function removeOwnedPublicationEntry(directory, basename, expected) {
|
|
90
|
+
if (!equal(await entry(directory, basename), expected))
|
|
91
|
+
return false;
|
|
92
|
+
await rm(`/proc/self/fd/${directory.fd}/${basename}`);
|
|
93
|
+
return (await entry(directory, basename)) === undefined;
|
|
94
|
+
}
|
|
95
|
+
export async function rollbackOwnedCommit(token, hooks = {}) {
|
|
96
|
+
if (token.kind === "creation")
|
|
97
|
+
return { state: "ambiguous", phase: "rollback" };
|
|
98
|
+
await hooks.beforeRollback?.();
|
|
99
|
+
if (!equal(await entry(token.directory, token.destinationBasename), token.destinationIdentity) ||
|
|
100
|
+
!equal(await entry(token.directory, token.displacedBasename), token.displacedIdentity))
|
|
101
|
+
return { state: "ambiguous", phase: "rollback" };
|
|
102
|
+
await exchangeDirectoryEntries(token.directory, token.displacedBasename, token.destinationBasename).catch(() => undefined);
|
|
103
|
+
const restored = equal(await entry(token.directory, token.destinationBasename), token.displacedIdentity) &&
|
|
104
|
+
equal(await entry(token.directory, token.displacedBasename), token.destinationIdentity);
|
|
105
|
+
if (!restored)
|
|
106
|
+
return { state: "ambiguous", phase: "rollback" };
|
|
107
|
+
return { state: "rolled_back", reason: "conflict" };
|
|
108
|
+
}
|
|
109
|
+
export async function publishAuthenticated(plan) {
|
|
110
|
+
if (process.platform !== "linux")
|
|
111
|
+
return { state: "pre_effect", reason: "unsupported" };
|
|
112
|
+
await plan.beforePublish?.();
|
|
113
|
+
if (plan.kind === "creation") {
|
|
114
|
+
await linkHeldFile(plan.stage, plan.directory, plan.destinationBasename).catch(() => undefined);
|
|
115
|
+
const destination = await entry(plan.directory, plan.destinationBasename);
|
|
116
|
+
if (equal(destination, plan.stageIdentity))
|
|
117
|
+
return {
|
|
118
|
+
state: "committed",
|
|
119
|
+
token: {
|
|
120
|
+
kind: "creation",
|
|
121
|
+
directory: plan.directory,
|
|
122
|
+
destinationBasename: plan.destinationBasename,
|
|
123
|
+
destinationIdentity: plan.stageIdentity,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
if (destination?.dev === plan.stageIdentity.dev && destination.ino === plan.stageIdentity.ino)
|
|
127
|
+
return { state: "ambiguous", phase: "commit" };
|
|
128
|
+
return { state: "pre_effect", reason: destination ? "conflict" : "unsupported" };
|
|
129
|
+
}
|
|
130
|
+
await exchangeDirectoryEntries(plan.directory, plan.stageBasename, plan.destinationBasename).catch(() => undefined);
|
|
131
|
+
const destination = await entry(plan.directory, plan.destinationBasename);
|
|
132
|
+
const displaced = await entry(plan.directory, plan.stageBasename);
|
|
133
|
+
if (equal(destination, plan.stageIdentity) && displaced) {
|
|
134
|
+
const token = {
|
|
135
|
+
kind: "replacement",
|
|
136
|
+
directory: plan.directory,
|
|
137
|
+
destinationBasename: plan.destinationBasename,
|
|
138
|
+
displacedBasename: plan.stageBasename,
|
|
139
|
+
destinationIdentity: destination,
|
|
140
|
+
displacedIdentity: displaced,
|
|
141
|
+
};
|
|
142
|
+
const held = plan.preimage && (await snapshotHeldFile(plan.preimage));
|
|
143
|
+
if (equal(displaced, plan.preimageIdentity) && equal(held, plan.preimageIdentity))
|
|
144
|
+
return { state: "committed", token };
|
|
145
|
+
return rollbackOwnedCommit(token, plan);
|
|
146
|
+
}
|
|
147
|
+
if (equal(destination, plan.preimageIdentity) && equal(displaced, plan.stageIdentity))
|
|
148
|
+
return { state: "pre_effect", reason: "unsupported" };
|
|
149
|
+
return { state: "ambiguous", phase: "commit" };
|
|
150
|
+
}
|
|
151
|
+
export async function probePublicationCapability(parent) {
|
|
152
|
+
const probe = path.join(parent, `.ast-publication-probe-${randomUUID()}`);
|
|
153
|
+
const handles = [];
|
|
154
|
+
try {
|
|
155
|
+
await mkdir(probe);
|
|
156
|
+
const directory = await open(probe, "r");
|
|
157
|
+
handles.push(directory);
|
|
158
|
+
for (const [name, bytes] of [
|
|
159
|
+
["stage", "stage"],
|
|
160
|
+
["replacement", "replacement"],
|
|
161
|
+
])
|
|
162
|
+
await writeFile(path.join(probe, name), bytes);
|
|
163
|
+
const stage = await open(path.join(probe, "stage"), "r");
|
|
164
|
+
handles.push(stage);
|
|
165
|
+
const replacement = await open(path.join(probe, "replacement"), "r");
|
|
166
|
+
handles.push(replacement);
|
|
167
|
+
const stageIdentity = await snapshotHeldFile(stage);
|
|
168
|
+
const replacementIdentity = await snapshotHeldFile(replacement);
|
|
169
|
+
await linkHeldFile(stage, directory, "created");
|
|
170
|
+
if (!equal(await entry(directory, "created"), stageIdentity))
|
|
171
|
+
throw new Error("Link unsupported");
|
|
172
|
+
await exchangeDirectoryEntries(directory, "replacement", "created");
|
|
173
|
+
if (!equal(await entry(directory, "created"), replacementIdentity) ||
|
|
174
|
+
!equal(await entry(directory, "replacement"), stageIdentity))
|
|
175
|
+
throw new Error("Publication exchange unsupported");
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await Promise.allSettled(handles.map((handle) => handle.close()));
|
|
179
|
+
await rm(probe, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -17,11 +17,7 @@ export function planCallSpines(root, edges, options) {
|
|
|
17
17
|
const maxEdges = assertLimit(options.max_edges, 1, "max_edges");
|
|
18
18
|
const accepted = edges.filter(exactCall);
|
|
19
19
|
const freshness = options.freshness?.state ?? edges[0]?.freshness.state;
|
|
20
|
-
const
|
|
21
|
-
? "untrusted"
|
|
22
|
-
: options.discovery_complete === false
|
|
23
|
-
? "incomplete"
|
|
24
|
-
: "authoritative";
|
|
20
|
+
const trusted = freshness === "fresh" && accepted.length === edges.length;
|
|
25
21
|
const reasons = new Set();
|
|
26
22
|
const rootKey = endpointKey(root);
|
|
27
23
|
const initial = { endpoint: root, endpoints: [root], relationshipIds: [], depth: 0 };
|
|
@@ -82,6 +78,15 @@ export function planCallSpines(root, edges, options) {
|
|
|
82
78
|
endpoints: record.endpoints,
|
|
83
79
|
relationship_ids: record.relationshipIds,
|
|
84
80
|
}));
|
|
81
|
+
const reached = new Set(records.keys());
|
|
82
|
+
const relevantGap = (options.unfinished_gaps ?? []).some((gap) => options.direction === "outgoing"
|
|
83
|
+
? reached.has(endpointKey(gap.source))
|
|
84
|
+
: gap.alternatives.some((alternative) => reached.has(endpointKey(alternative))));
|
|
85
|
+
const authorityState = !trusted
|
|
86
|
+
? "untrusted"
|
|
87
|
+
: options.discovery_complete === false || relevantGap
|
|
88
|
+
? "incomplete"
|
|
89
|
+
: "authoritative";
|
|
85
90
|
const incomplete = authorityState !== "authoritative" || reasons.size > 0;
|
|
86
91
|
return {
|
|
87
92
|
root,
|
|
@@ -167,7 +167,8 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
167
167
|
max_depth: request.callSpines.maxDepth,
|
|
168
168
|
max_nodes: request.callSpines.maxNodes,
|
|
169
169
|
max_edges: request.callSpines.maxEdges,
|
|
170
|
-
discovery_complete: !projected.
|
|
170
|
+
discovery_complete: !projected.bounded_incomplete,
|
|
171
|
+
unfinished_gaps: projected.unfinished_gaps,
|
|
171
172
|
freshness,
|
|
172
173
|
});
|
|
173
174
|
}
|