@vincemakes/kiso-tools-node 0.13.0 → 0.15.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 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: string;
116
- replace: string;
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.13.0",
3
+ "version": "0.15.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.13.0"
24
+ "@vincemakes/kiso-core": "0.15.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",