@shrkcrft/generator 0.1.0-alpha.3 → 0.1.0-alpha.30
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/dry-run.d.ts.map +1 -1
- package/dist/dry-run.js +28 -8
- package/dist/file-change.d.ts +1 -1
- package/dist/file-change.d.ts.map +1 -1
- package/dist/folder-apply.d.ts.map +1 -1
- package/dist/folder-apply.js +46 -2
- package/dist/folder-safety.d.ts.map +1 -1
- package/dist/folder-safety.js +11 -0
- package/dist/generator-engine.d.ts.map +1 -1
- package/dist/generator-engine.js +26 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/operations.d.ts +156 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +5 -0
- package/dist/package-delegate-plan.d.ts +54 -0
- package/dist/package-delegate-plan.d.ts.map +1 -0
- package/dist/package-delegate-plan.js +253 -0
- package/dist/planned-change.d.ts +2 -123
- package/dist/planned-change.d.ts.map +1 -1
- package/dist/planned-change.js +104 -0
- package/dist/saved-plan.d.ts +28 -2
- package/dist/saved-plan.d.ts.map +1 -1
- package/dist/saved-plan.js +20 -0
- package/dist/synthetic-plan.d.ts.map +1 -1
- package/dist/synthetic-plan.js +89 -18
- package/package.json +6 -6
package/dist/synthetic-plan.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import * as nodePath from 'node:path';
|
|
18
|
+
import { safeResolveTargetPath } from '@shrkcrft/core';
|
|
18
19
|
import { evaluatePlannedChange } from "./planned-change.js";
|
|
19
20
|
import { FileChangeType } from "./file-change.js";
|
|
20
21
|
export const SYNTHETIC_TEMPLATE_PREFIX = '__';
|
|
@@ -26,6 +27,14 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
26
27
|
const warnings = [];
|
|
27
28
|
let hasConflicts = false;
|
|
28
29
|
const expected = plan.expectedChanges ?? [];
|
|
30
|
+
// Per-file content overlay so MULTIPLE ops on the SAME file COMPOSE: each op
|
|
31
|
+
// sees the cumulative result of the prior ops on that path rather than the
|
|
32
|
+
// original on-disk bytes. Mirrors the template path in dry-run.ts. Without
|
|
33
|
+
// it, [append A, append B] loses A (the writer's last same-path write
|
|
34
|
+
// clobbers earlier ones) and create-then-modify on one NEW path
|
|
35
|
+
// false-conflicts (op2 reads stale disk). Keyed by absolute path → the
|
|
36
|
+
// cumulative content after the prior op(s).
|
|
37
|
+
const overlay = new Map();
|
|
29
38
|
for (const e of expected) {
|
|
30
39
|
if (!e.operation) {
|
|
31
40
|
// Without an operation we cannot reconstruct contents — surface as
|
|
@@ -42,7 +51,7 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
42
51
|
hasConflicts = true;
|
|
43
52
|
continue;
|
|
44
53
|
}
|
|
45
|
-
const change = applyOperation(projectRoot, e.relativePath, e.operation);
|
|
54
|
+
const change = applyOperation(projectRoot, e.relativePath, e.operation, overlay);
|
|
46
55
|
if (change.type === FileChangeType.Conflict)
|
|
47
56
|
hasConflicts = true;
|
|
48
57
|
changes.push(change);
|
|
@@ -61,15 +70,44 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
|
|
|
61
70
|
}
|
|
62
71
|
return out;
|
|
63
72
|
}
|
|
64
|
-
function applyOperation(projectRoot, relativePath, operation) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
function applyOperation(projectRoot, relativePath, operation, overlay) {
|
|
74
|
+
// Route through the single generator chokepoint instead of a bare resolve, so
|
|
75
|
+
// a traversal / absolute `relativePath` in a hand-crafted or tampered plan
|
|
76
|
+
// can't write OUTSIDE the project root. An unsafe path becomes a Conflict,
|
|
77
|
+
// which writeSyntheticPlan refuses (matching the template path in dry-run.ts).
|
|
78
|
+
let safe;
|
|
79
|
+
try {
|
|
80
|
+
safe = safeResolveTargetPath(relativePath, projectRoot);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
const pathErr = e;
|
|
84
|
+
return {
|
|
85
|
+
type: FileChangeType.Conflict,
|
|
86
|
+
absolutePath: pathErr.rawPath,
|
|
87
|
+
relativePath: pathErr.rawPath,
|
|
88
|
+
contents: '',
|
|
89
|
+
reason: `Refused unsafe target path (${pathErr.code}): ${pathErr.message}`,
|
|
90
|
+
sizeBytes: 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Prefer the overlay (the cumulative result of prior same-file ops) over the
|
|
94
|
+
// on-disk bytes so op N sees the result of ops 1..N-1. Falls back to the live
|
|
95
|
+
// file, then to absent (`null`) for a brand-new path.
|
|
96
|
+
const existing = overlay.has(safe.absolutePath)
|
|
97
|
+
? (overlay.get(safe.absolutePath) ?? null)
|
|
98
|
+
: existsSync(safe.absolutePath)
|
|
99
|
+
? readFileSync(safe.absolutePath, 'utf8')
|
|
100
|
+
: null;
|
|
101
|
+
const result = evaluatePlannedChange({
|
|
102
|
+
change: { targetPath: safe.relativePath, operation },
|
|
103
|
+
absolutePath: safe.absolutePath,
|
|
104
|
+
relativePath: safe.relativePath,
|
|
71
105
|
existing,
|
|
72
106
|
});
|
|
107
|
+
// Record the cumulative content (Skip/Conflict carry the unchanged bytes,
|
|
108
|
+
// which is exactly what a later op on the same file should see).
|
|
109
|
+
overlay.set(safe.absolutePath, result.contents);
|
|
110
|
+
return result;
|
|
73
111
|
}
|
|
74
112
|
/**
|
|
75
113
|
* Write evaluated changes from a synthetic plan directly. The caller
|
|
@@ -78,30 +116,63 @@ function applyOperation(projectRoot, relativePath, operation) {
|
|
|
78
116
|
*/
|
|
79
117
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
80
118
|
import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
|
|
119
|
+
/**
|
|
120
|
+
* A CCR retrieval marker (`<<ccr:<hex>…>>`) is a pointer into the compress
|
|
121
|
+
* cache — a LOSSY/compressed view, never apply-grade source. If one ever
|
|
122
|
+
* reaches a write (e.g. a compressed diff fed into a `create`/`replace` op), it
|
|
123
|
+
* would corrupt the file. This detector enforces, at the write chokepoint, the
|
|
124
|
+
* invariant that the compression layer only documents in a comment.
|
|
125
|
+
*/
|
|
126
|
+
const CCR_MARKER_RE = /<<ccr:[0-9a-f]{8,}/;
|
|
81
127
|
export function writeSyntheticPlan(plan) {
|
|
82
128
|
if (plan.hasConflicts) {
|
|
83
129
|
return err(new AppErrorImpl(ERROR_CODES.TARGET_FILE_EXISTS, 'Synthetic plan refused: conflicts present', { details: { conflicts: plan.changes.filter((c) => c.type === FileChangeType.Conflict) } }));
|
|
84
130
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let skipped = 0;
|
|
131
|
+
// Refuse the WHOLE plan if any writeable change carries a CCR marker — a
|
|
132
|
+
// compressed/lossy blob must never be written as source.
|
|
88
133
|
for (const change of plan.changes) {
|
|
89
|
-
if (change.type === FileChangeType.Skip) {
|
|
90
|
-
skipped += 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
134
|
if (!isWriteableSyntheticChange(change.type))
|
|
94
135
|
continue;
|
|
136
|
+
if (CCR_MARKER_RE.test(change.contents)) {
|
|
137
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Refused to write ${change.relativePath}: contents carry a <<ccr:…>> marker (a compressed/lossy blob is not apply-grade source).`, { details: { path: change.relativePath } }));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Compose multiple ops on the SAME path into ONE write. `evaluateSavedPlanInPlace`
|
|
141
|
+
// threads an overlay, so the LAST writeable change for a path already carries
|
|
142
|
+
// the cumulative result of every prior same-path op (any trailing Skip leaves
|
|
143
|
+
// those bytes unchanged). Writing that once — instead of every op's contents
|
|
144
|
+
// in order, last-write-wins — avoids redundant writes and keeps the success
|
|
145
|
+
// count honest: `written` and `summary.written` count DISTINCT paths.
|
|
146
|
+
const lastWriteableByPath = new Map();
|
|
147
|
+
const skipPaths = new Set();
|
|
148
|
+
for (const change of plan.changes) {
|
|
149
|
+
if (isWriteableSyntheticChange(change.type)) {
|
|
150
|
+
lastWriteableByPath.set(change.absolutePath, change);
|
|
151
|
+
}
|
|
152
|
+
else if (change.type === FileChangeType.Skip) {
|
|
153
|
+
skipPaths.add(change.absolutePath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const written = [];
|
|
157
|
+
let totalBytes = 0;
|
|
158
|
+
for (const [absolutePath, change] of lastWriteableByPath) {
|
|
95
159
|
try {
|
|
96
|
-
mkdirSync(nodePath.dirname(
|
|
97
|
-
writeFileSync(
|
|
160
|
+
mkdirSync(nodePath.dirname(absolutePath), { recursive: true });
|
|
161
|
+
writeFileSync(absolutePath, change.contents, 'utf8');
|
|
98
162
|
written.push(change);
|
|
99
163
|
totalBytes += change.sizeBytes;
|
|
100
164
|
}
|
|
101
165
|
catch (e) {
|
|
102
|
-
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${
|
|
166
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to write ${absolutePath}`, { details: { path: absolutePath }, cause: e }));
|
|
103
167
|
}
|
|
104
168
|
}
|
|
169
|
+
// A path counts as skipped only when EVERY op on it resolved to Skip (a path
|
|
170
|
+
// that was also written is not a skip).
|
|
171
|
+
let skipped = 0;
|
|
172
|
+
for (const p of skipPaths) {
|
|
173
|
+
if (!lastWriteableByPath.has(p))
|
|
174
|
+
skipped += 1;
|
|
175
|
+
}
|
|
105
176
|
return ok({
|
|
106
177
|
summary: { written: written.length, skipped, conflicts: 0, totalBytes },
|
|
107
178
|
written,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/generator",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.30",
|
|
4
4
|
"description": "SharkCraft plan-first generator: GenerationPlan, FileChange, dry-run, safe writes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
|
-
"types": "./dist/index.d.
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
47
|
-
"@shrkcrft/templates": "^0.1.0-alpha.
|
|
48
|
-
"@shrkcrft/rules": "^0.1.0-alpha.
|
|
49
|
-
"@shrkcrft/paths": "^0.1.0-alpha.
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.30",
|
|
47
|
+
"@shrkcrft/templates": "^0.1.0-alpha.30",
|
|
48
|
+
"@shrkcrft/rules": "^0.1.0-alpha.30",
|
|
49
|
+
"@shrkcrft/paths": "^0.1.0-alpha.30"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|