@vincemakes/kiso-tools-node 0.13.0 → 0.14.0
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/index.d.ts +8 -2
- package/dist/index.js +0 -0
- package/dist/wr1.d.ts +59 -0
- package/dist/wr1.js +109 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -109,11 +109,17 @@ export declare function searchTextTool(opts: WorkspaceToolsOptions): Tool<{
|
|
|
109
109
|
export declare function writeFileTool(opts: WorkspaceToolsOptions): Tool<{
|
|
110
110
|
path: string;
|
|
111
111
|
content: string;
|
|
112
|
+
expectedRevision?: string;
|
|
112
113
|
}>;
|
|
113
114
|
export declare function editFileTool(opts: WorkspaceToolsOptions): Tool<{
|
|
114
115
|
path: string;
|
|
115
|
-
search
|
|
116
|
-
replace
|
|
116
|
+
search?: string;
|
|
117
|
+
replace?: string;
|
|
118
|
+
edits?: readonly {
|
|
119
|
+
search: string;
|
|
120
|
+
replace: string;
|
|
121
|
+
}[];
|
|
122
|
+
expectedRevision?: string;
|
|
117
123
|
}>;
|
|
118
124
|
export declare function shellTool(opts: WorkspaceToolsOptions): Tool<{
|
|
119
125
|
command: string;
|
package/dist/index.js
CHANGED
|
Binary file
|
package/dist/wr1.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WR-1 — the observed-revision stale-write guard's primitives, extracted
|
|
3
|
+
* so the classification rules are unit-testable (WR-1A).
|
|
4
|
+
*
|
|
5
|
+
* The revision is a CONTENT-STATE WITNESS, never an epistemic proof: it
|
|
6
|
+
* answers "has this file changed since the citation was issued", not
|
|
7
|
+
* "did the model read this exact file" (no path binding — two files
|
|
8
|
+
* with identical bytes share a token, harmlessly: the current bytes ARE
|
|
9
|
+
* the cited state). The guard is against accidents, not adversaries.
|
|
10
|
+
*/
|
|
11
|
+
import type { ToolResult } from "@vincemakes/kiso-core";
|
|
12
|
+
/** sha256 over RAW bytes (never the decoded string — UTF-8 replacement
|
|
13
|
+
* semantics would lie about the world), truncated to a 64-bit citation
|
|
14
|
+
* token. Truncation loses nothing the full digest would keep here: a
|
|
15
|
+
* non-cooperating writer wins the conditional-write window without
|
|
16
|
+
* ever touching the hash. */
|
|
17
|
+
export declare function contentRevision(bytes: Buffer): string;
|
|
18
|
+
/** The stale-write refusal: work refused BEFORE it starts — exactly what
|
|
19
|
+
* the `precondition` kind exists to say (and the reason the kernel's
|
|
20
|
+
* partial-effects note never rides one). */
|
|
21
|
+
export declare function precondition(content: string): ToolResult;
|
|
22
|
+
/**
|
|
23
|
+
* WR-1-F2 — citation normalization (found by the rel-0140 blocking
|
|
24
|
+
* bench): the old trailer `[rev: rev:X]` double-labeled the token, and
|
|
25
|
+
* DeepSeek-class models flailed between citing `rev:rev:X` and bare
|
|
26
|
+
* `X` — every first edit refused, 2–7 wasted rounds per file. The
|
|
27
|
+
* trailer is now `[rev:X]`, and EVERY plausible copy of the token
|
|
28
|
+
* normalizes to the same citation: tolerance in the reader,
|
|
29
|
+
* strictness in the comparison — never a bypass (a wrong token stays
|
|
30
|
+
* wrong in every form).
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeRevision(cited: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* WR-1A ①: a POST-EFFECT verification failure is FATAL — the rename
|
|
35
|
+
* already happened, so `precondition` ("nothing ran") would make the
|
|
36
|
+
* durable receipt lie about the world. The copy says the effect may
|
|
37
|
+
* already have applied, and the kernel's non-idempotent note rides it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function postEffectEscape(action: "write" | "edit", path: string): ToolResult;
|
|
40
|
+
/**
|
|
41
|
+
* WR-1A ②: the CREATION publish — link(2) or refusal, never a fallback.
|
|
42
|
+
* link is atomic no-clobber: target absent → the entry appears; target
|
|
43
|
+
* present → EEXIST, the loser is told LOUDLY. Any other failure refuses
|
|
44
|
+
* too (fail closed): a platform that cannot give the atomic primitive
|
|
45
|
+
* does not get to degrade "absent" into a clobber-capable rename — a
|
|
46
|
+
* semantic guarantee that is unavailable is an honest refusal, never a
|
|
47
|
+
* silent downgrade. Returns null on success; the temp is consumed
|
|
48
|
+
* (linked then unlinked) or cleaned on every failure path.
|
|
49
|
+
*/
|
|
50
|
+
export declare function publishNewFile(tmp: string, full: string, path: string): ToolResult | null;
|
|
51
|
+
/**
|
|
52
|
+
* WR-1A ③: the immediate pre-rename revalidation — after staging, right
|
|
53
|
+
* before the replacement commits, the target must STILL hash to the
|
|
54
|
+
* cited revision. This shrinks the conditional-write window from
|
|
55
|
+
* validate→stage→chmod→rename to check→rename (POSIX offers no
|
|
56
|
+
* rename-if-content-equals, so the residual window is the narrowed
|
|
57
|
+
* claim, stated as such). Returns null when the world still matches.
|
|
58
|
+
*/
|
|
59
|
+
export declare function revalidateBeforeRename(full: string, expectedRevision: string, tool: "write_file" | "edit_file", path: string): ToolResult | null;
|
package/dist/wr1.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WR-1 — the observed-revision stale-write guard's primitives, extracted
|
|
3
|
+
* so the classification rules are unit-testable (WR-1A).
|
|
4
|
+
*
|
|
5
|
+
* The revision is a CONTENT-STATE WITNESS, never an epistemic proof: it
|
|
6
|
+
* answers "has this file changed since the citation was issued", not
|
|
7
|
+
* "did the model read this exact file" (no path binding — two files
|
|
8
|
+
* with identical bytes share a token, harmlessly: the current bytes ARE
|
|
9
|
+
* the cited state). The guard is against accidents, not adversaries.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { linkSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
|
+
/** sha256 over RAW bytes (never the decoded string — UTF-8 replacement
|
|
14
|
+
* semantics would lie about the world), truncated to a 64-bit citation
|
|
15
|
+
* token. Truncation loses nothing the full digest would keep here: a
|
|
16
|
+
* non-cooperating writer wins the conditional-write window without
|
|
17
|
+
* ever touching the hash. */
|
|
18
|
+
export function contentRevision(bytes) {
|
|
19
|
+
return `rev:${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`;
|
|
20
|
+
}
|
|
21
|
+
/** The stale-write refusal: work refused BEFORE it starts — exactly what
|
|
22
|
+
* the `precondition` kind exists to say (and the reason the kernel's
|
|
23
|
+
* partial-effects note never rides one). */
|
|
24
|
+
export function precondition(content) {
|
|
25
|
+
return { content, isError: true, errorKind: "precondition" };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* WR-1-F2 — citation normalization (found by the rel-0140 blocking
|
|
29
|
+
* bench): the old trailer `[rev: rev:X]` double-labeled the token, and
|
|
30
|
+
* DeepSeek-class models flailed between citing `rev:rev:X` and bare
|
|
31
|
+
* `X` — every first edit refused, 2–7 wasted rounds per file. The
|
|
32
|
+
* trailer is now `[rev:X]`, and EVERY plausible copy of the token
|
|
33
|
+
* normalizes to the same citation: tolerance in the reader,
|
|
34
|
+
* strictness in the comparison — never a bypass (a wrong token stays
|
|
35
|
+
* wrong in every form).
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeRevision(cited) {
|
|
38
|
+
const t = cited.trim();
|
|
39
|
+
if (t === "absent")
|
|
40
|
+
return t;
|
|
41
|
+
if (/^rev:rev:[0-9a-f]{16}$/.test(t))
|
|
42
|
+
return t.slice(4);
|
|
43
|
+
if (/^[0-9a-f]{16}$/.test(t))
|
|
44
|
+
return `rev:${t}`;
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* WR-1A ①: a POST-EFFECT verification failure is FATAL — the rename
|
|
49
|
+
* already happened, so `precondition` ("nothing ran") would make the
|
|
50
|
+
* durable receipt lie about the world. The copy says the effect may
|
|
51
|
+
* already have applied, and the kernel's non-idempotent note rides it.
|
|
52
|
+
*/
|
|
53
|
+
export function postEffectEscape(action, path) {
|
|
54
|
+
return {
|
|
55
|
+
content: `${action}_file failed: ${path} escaped the workspace after replacement — the ${action} may already have applied`,
|
|
56
|
+
isError: true,
|
|
57
|
+
errorKind: "fatal",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* WR-1A ②: the CREATION publish — link(2) or refusal, never a fallback.
|
|
62
|
+
* link is atomic no-clobber: target absent → the entry appears; target
|
|
63
|
+
* present → EEXIST, the loser is told LOUDLY. Any other failure refuses
|
|
64
|
+
* too (fail closed): a platform that cannot give the atomic primitive
|
|
65
|
+
* does not get to degrade "absent" into a clobber-capable rename — a
|
|
66
|
+
* semantic guarantee that is unavailable is an honest refusal, never a
|
|
67
|
+
* silent downgrade. Returns null on success; the temp is consumed
|
|
68
|
+
* (linked then unlinked) or cleaned on every failure path.
|
|
69
|
+
*/
|
|
70
|
+
export function publishNewFile(tmp, full, path) {
|
|
71
|
+
try {
|
|
72
|
+
linkSync(tmp, full);
|
|
73
|
+
unlinkSync(tmp);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
try {
|
|
78
|
+
unlinkSync(tmp);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// already gone
|
|
82
|
+
}
|
|
83
|
+
if (err.code === "EEXIST") {
|
|
84
|
+
return precondition(`write_file: ${path} already exists — read it and pass its revision to replace it`);
|
|
85
|
+
}
|
|
86
|
+
return precondition(`write_file: cannot publish ${path} atomically on this filesystem (${err.code ?? "link failed"}) — the no-clobber creation guarantee cannot be honored here`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* WR-1A ③: the immediate pre-rename revalidation — after staging, right
|
|
91
|
+
* before the replacement commits, the target must STILL hash to the
|
|
92
|
+
* cited revision. This shrinks the conditional-write window from
|
|
93
|
+
* validate→stage→chmod→rename to check→rename (POSIX offers no
|
|
94
|
+
* rename-if-content-equals, so the residual window is the narrowed
|
|
95
|
+
* claim, stated as such). Returns null when the world still matches.
|
|
96
|
+
*/
|
|
97
|
+
export function revalidateBeforeRename(full, expectedRevision, tool, path) {
|
|
98
|
+
let current;
|
|
99
|
+
try {
|
|
100
|
+
current = contentRevision(readFileSync(full));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return precondition(`${tool}: ${path} no longer exists — it changed after validation; read it again`);
|
|
104
|
+
}
|
|
105
|
+
if (current !== expectedRevision) {
|
|
106
|
+
return precondition(`${tool}: ${path} changed since ${expectedRevision} — read it again and cite its [rev:…] line, then re-apply the change`);
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "kiso coding tools for Node hosts — read file, list directory, search text, write/edit file, shell command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.
|
|
24
|
+
"@vincemakes/kiso-core": "0.14.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|