@shrkcrft/generator 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/conflict-handler.d.ts +12 -0
- package/dist/conflict-handler.d.ts.map +1 -0
- package/dist/conflict-handler.js +25 -0
- package/dist/dry-run.d.ts +10 -0
- package/dist/dry-run.d.ts.map +1 -0
- package/dist/dry-run.js +178 -0
- package/dist/file-change.d.ts +46 -0
- package/dist/file-change.d.ts.map +1 -0
- package/dist/file-change.js +22 -0
- package/dist/folder-apply.d.ts +29 -0
- package/dist/folder-apply.d.ts.map +1 -0
- package/dist/folder-apply.js +117 -0
- package/dist/folder-safety.d.ts +12 -0
- package/dist/folder-safety.d.ts.map +1 -0
- package/dist/folder-safety.js +75 -0
- package/dist/generation-plan.d.ts +24 -0
- package/dist/generation-plan.d.ts.map +1 -0
- package/dist/generation-plan.js +1 -0
- package/dist/generation-request.d.ts +14 -0
- package/dist/generation-request.d.ts.map +1 -0
- package/dist/generation-request.js +1 -0
- package/dist/generator-engine.d.ts +12 -0
- package/dist/generator-engine.d.ts.map +1 -0
- package/dist/generator-engine.js +74 -0
- package/dist/grounding/extracted-plan.d.ts +42 -0
- package/dist/grounding/extracted-plan.d.ts.map +1 -0
- package/dist/grounding/extracted-plan.js +12 -0
- package/dist/grounding/extractor-registry.d.ts +21 -0
- package/dist/grounding/extractor-registry.d.ts.map +1 -0
- package/dist/grounding/extractor-registry.js +30 -0
- package/dist/grounding/extractor.d.ts +24 -0
- package/dist/grounding/extractor.d.ts.map +1 -0
- package/dist/grounding/extractor.js +8 -0
- package/dist/grounding/extractors/markdown-frontmatter-loose.d.ts +17 -0
- package/dist/grounding/extractors/markdown-frontmatter-loose.d.ts.map +1 -0
- package/dist/grounding/extractors/markdown-frontmatter-loose.js +160 -0
- package/dist/grounding/extractors/sharkcraft-spec-v1.d.ts +12 -0
- package/dist/grounding/extractors/sharkcraft-spec-v1.d.ts.map +1 -0
- package/dist/grounding/extractors/sharkcraft-spec-v1.js +56 -0
- package/dist/grounding/index.d.ts +6 -0
- package/dist/grounding/index.d.ts.map +1 -0
- package/dist/grounding/index.js +5 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/naming-strategy.d.ts +5 -0
- package/dist/naming-strategy.d.ts.map +1 -0
- package/dist/naming-strategy.js +28 -0
- package/dist/overwrite-strategy.d.ts +14 -0
- package/dist/overwrite-strategy.d.ts.map +1 -0
- package/dist/overwrite-strategy.js +15 -0
- package/dist/plan-signing.d.ts +37 -0
- package/dist/plan-signing.d.ts.map +1 -0
- package/dist/plan-signing.js +82 -0
- package/dist/planned-change.d.ts +167 -0
- package/dist/planned-change.d.ts.map +1 -0
- package/dist/planned-change.js +507 -0
- package/dist/saved-plan.d.ts +110 -0
- package/dist/saved-plan.d.ts.map +1 -0
- package/dist/saved-plan.js +281 -0
- package/dist/spec/index.d.ts +7 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +6 -0
- package/dist/spec/spec-derive.d.ts +15 -0
- package/dist/spec/spec-derive.d.ts.map +1 -0
- package/dist/spec/spec-derive.js +294 -0
- package/dist/spec/spec-frontmatter.d.ts +37 -0
- package/dist/spec/spec-frontmatter.d.ts.map +1 -0
- package/dist/spec/spec-frontmatter.js +497 -0
- package/dist/spec/spec-id.d.ts +30 -0
- package/dist/spec/spec-id.d.ts.map +1 -0
- package/dist/spec/spec-id.js +38 -0
- package/dist/spec/spec-io.d.ts +56 -0
- package/dist/spec/spec-io.d.ts.map +1 -0
- package/dist/spec/spec-io.js +176 -0
- package/dist/spec/spec-model.d.ts +117 -0
- package/dist/spec/spec-model.d.ts.map +1 -0
- package/dist/spec/spec-model.js +225 -0
- package/dist/spec/spec-scaffold.d.ts +32 -0
- package/dist/spec/spec-scaffold.d.ts.map +1 -0
- package/dist/spec/spec-scaffold.js +106 -0
- package/dist/synthetic-plan.d.ts +14 -0
- package/dist/synthetic-plan.d.ts.map +1 -0
- package/dist/synthetic-plan.js +123 -0
- package/package.json +54 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planned change model — v2.
|
|
3
|
+
*
|
|
4
|
+
* v1 templates emit CREATE-only entries via `files() → ITemplateFile[]`. The
|
|
5
|
+
* v2 model lets templates declare explicit UPDATE-style operations (append,
|
|
6
|
+
* insert-after, insert-before, replace, export) alongside CREATE entries.
|
|
7
|
+
*
|
|
8
|
+
* Each operation:
|
|
9
|
+
* - is declared by the template as a small structured intent;
|
|
10
|
+
* - is evaluated at plan time against the live file system;
|
|
11
|
+
* - resolves to a concrete `IFileChange` with precomputed final contents,
|
|
12
|
+
* a kind ("append" / "insert-after" / ... / "skip" / "conflict"),
|
|
13
|
+
* and a reason that explains what the engine decided.
|
|
14
|
+
*
|
|
15
|
+
* Hard rules preserved by this module:
|
|
16
|
+
* - No arbitrary code execution. Operations are data, not closures.
|
|
17
|
+
* - No hidden post-apply hooks. Every byte written ends up in `contents` and
|
|
18
|
+
* is shown in dry-run / plan review.
|
|
19
|
+
* - Same write path as v1: `generator-engine.generate()` writes the
|
|
20
|
+
* precomputed `contents`. Apply does not re-interpret the operation.
|
|
21
|
+
* - MCP stays read-only — this module is pure logic.
|
|
22
|
+
*/
|
|
23
|
+
import { FileChangeType } from "./file-change.js";
|
|
24
|
+
export function evaluatePlannedChange(input) {
|
|
25
|
+
const { change, absolutePath, relativePath, existing } = input;
|
|
26
|
+
const op = change.operation;
|
|
27
|
+
switch (op.kind) {
|
|
28
|
+
case 'create':
|
|
29
|
+
return evaluateCreate(op, absolutePath, relativePath, existing);
|
|
30
|
+
case 'append':
|
|
31
|
+
return evaluateAppend(op, absolutePath, relativePath, existing);
|
|
32
|
+
case 'insert-after':
|
|
33
|
+
return evaluateInsertAt(op, 'after', absolutePath, relativePath, existing);
|
|
34
|
+
case 'insert-before':
|
|
35
|
+
return evaluateInsertAt(op, 'before', absolutePath, relativePath, existing);
|
|
36
|
+
case 'replace':
|
|
37
|
+
return evaluateReplace(op, absolutePath, relativePath, existing);
|
|
38
|
+
case 'export':
|
|
39
|
+
return evaluateExport(op, absolutePath, relativePath, existing);
|
|
40
|
+
case 'ensure-import':
|
|
41
|
+
return evaluateEnsureImport(op, absolutePath, relativePath, existing);
|
|
42
|
+
case 'insert-enum-entry':
|
|
43
|
+
return evaluateInsertEnumEntry(op, absolutePath, relativePath, existing);
|
|
44
|
+
case 'insert-object-entry':
|
|
45
|
+
return evaluateInsertObjectEntry(op, absolutePath, relativePath, existing);
|
|
46
|
+
case 'insert-before-closing-brace':
|
|
47
|
+
return evaluateInsertBeforeClosingBrace(op, absolutePath, relativePath, existing);
|
|
48
|
+
case 'insert-between-anchors':
|
|
49
|
+
return evaluateInsertBetweenAnchors(op, absolutePath, relativePath, existing);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function evaluateCreate(op, absolutePath, relativePath, existing) {
|
|
53
|
+
if (existing !== null) {
|
|
54
|
+
if (existing === op.content) {
|
|
55
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, op.content, 'No changes (identical contents)', op);
|
|
56
|
+
}
|
|
57
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, op.content, 'overwrite strategy: never (file exists)', op);
|
|
58
|
+
}
|
|
59
|
+
return mkChange(FileChangeType.Create, absolutePath, relativePath, op.content, 'New file (does not exist)', op);
|
|
60
|
+
}
|
|
61
|
+
function evaluateAppend(op, absolutePath, relativePath, existing) {
|
|
62
|
+
if (existing === null) {
|
|
63
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, op.snippet, 'append: target file does not exist', op);
|
|
64
|
+
}
|
|
65
|
+
const marker = op.ifMissing ?? op.snippet;
|
|
66
|
+
if (marker.length > 0 && existing.includes(marker)) {
|
|
67
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'append: snippet already present (idempotent)', op);
|
|
68
|
+
}
|
|
69
|
+
const next = existing.endsWith('\n') ? existing + op.snippet : existing + '\n' + op.snippet;
|
|
70
|
+
return mkChange(FileChangeType.Append, absolutePath, relativePath, next, `append +${byteLen(op.snippet)}B`, op);
|
|
71
|
+
}
|
|
72
|
+
function evaluateInsertAt(op, position, absolutePath, relativePath, existing) {
|
|
73
|
+
if (existing === null) {
|
|
74
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, op.snippet, `insert-${position}: target file does not exist`, op);
|
|
75
|
+
}
|
|
76
|
+
const marker = op.ifMissing ?? op.snippet;
|
|
77
|
+
if (marker.length > 0 && existing.includes(marker)) {
|
|
78
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, `insert-${position}: snippet already present (idempotent)`, op);
|
|
79
|
+
}
|
|
80
|
+
const idx = existing.indexOf(op.anchor);
|
|
81
|
+
if (idx < 0) {
|
|
82
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-${position}: anchor not found`, op);
|
|
83
|
+
}
|
|
84
|
+
// Ambiguity: multiple anchors → conflict (caller must be explicit).
|
|
85
|
+
if (existing.indexOf(op.anchor, idx + op.anchor.length) >= 0) {
|
|
86
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-${position}: anchor matches multiple sites (ambiguous)`, op);
|
|
87
|
+
}
|
|
88
|
+
const cut = position === 'after' ? idx + op.anchor.length : idx;
|
|
89
|
+
const next = existing.slice(0, cut) + op.snippet + existing.slice(cut);
|
|
90
|
+
const type = position === 'after' ? FileChangeType.InsertAfter : FileChangeType.InsertBefore;
|
|
91
|
+
return mkChange(type, absolutePath, relativePath, next, `insert-${position} +${byteLen(op.snippet)}B`, op);
|
|
92
|
+
}
|
|
93
|
+
function evaluateReplace(op, absolutePath, relativePath, existing) {
|
|
94
|
+
if (existing === null) {
|
|
95
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, op.replaceWith, 'replace: target file does not exist', op);
|
|
96
|
+
}
|
|
97
|
+
// Already-applied detection: if the file already contains the replacement
|
|
98
|
+
// and not the original `find`, we treat as Skip (idempotent).
|
|
99
|
+
const findCount = countOccurrences(existing, op.find);
|
|
100
|
+
const replaceCount = countOccurrences(existing, op.replaceWith);
|
|
101
|
+
if (findCount === 0 && replaceCount > 0) {
|
|
102
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'replace: already applied', op);
|
|
103
|
+
}
|
|
104
|
+
if (findCount === 0) {
|
|
105
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, 'replace: find text not found', op);
|
|
106
|
+
}
|
|
107
|
+
const expected = op.expectMatches ?? 1;
|
|
108
|
+
if (findCount !== expected) {
|
|
109
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `replace: expected ${expected} match(es), found ${findCount}`, op);
|
|
110
|
+
}
|
|
111
|
+
const next = replaceAllLiteral(existing, op.find, op.replaceWith);
|
|
112
|
+
return mkChange(FileChangeType.Replace, absolutePath, relativePath, next, `replace ${findCount}×`, op);
|
|
113
|
+
}
|
|
114
|
+
function evaluateExport(op, absolutePath, relativePath, existing) {
|
|
115
|
+
if (existing === null) {
|
|
116
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, buildExportLine(op), 'export: target barrel file does not exist', op);
|
|
117
|
+
}
|
|
118
|
+
const line = buildExportLine(op);
|
|
119
|
+
const marker = op.ifMissing ?? line;
|
|
120
|
+
if (existing.includes(marker)) {
|
|
121
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'export: already present (idempotent)', op);
|
|
122
|
+
}
|
|
123
|
+
const next = existing.endsWith('\n') ? existing + line + '\n' : existing + '\n' + line + '\n';
|
|
124
|
+
return mkChange(FileChangeType.Export, absolutePath, relativePath, next, `export +1 line`, op);
|
|
125
|
+
}
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
// Helpers
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
function mkChange(type, absolutePath, relativePath, contents, reason, operation) {
|
|
130
|
+
return {
|
|
131
|
+
type,
|
|
132
|
+
absolutePath,
|
|
133
|
+
relativePath,
|
|
134
|
+
contents,
|
|
135
|
+
reason,
|
|
136
|
+
sizeBytes: byteLen(contents),
|
|
137
|
+
operation,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function byteLen(s) {
|
|
141
|
+
return Buffer.byteLength(s, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
function countOccurrences(haystack, needle) {
|
|
144
|
+
if (needle.length === 0)
|
|
145
|
+
return 0;
|
|
146
|
+
let count = 0;
|
|
147
|
+
let from = 0;
|
|
148
|
+
while (true) {
|
|
149
|
+
const i = haystack.indexOf(needle, from);
|
|
150
|
+
if (i < 0)
|
|
151
|
+
return count;
|
|
152
|
+
count += 1;
|
|
153
|
+
from = i + needle.length;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function replaceAllLiteral(haystack, find, replaceWith) {
|
|
157
|
+
if (find.length === 0)
|
|
158
|
+
return haystack;
|
|
159
|
+
let out = '';
|
|
160
|
+
let from = 0;
|
|
161
|
+
while (true) {
|
|
162
|
+
const i = haystack.indexOf(find, from);
|
|
163
|
+
if (i < 0) {
|
|
164
|
+
out += haystack.slice(from);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
out += haystack.slice(from, i) + replaceWith;
|
|
168
|
+
from = i + find.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function buildExportLine(op) {
|
|
172
|
+
if (op.symbols && op.symbols.length > 0) {
|
|
173
|
+
return `export { ${op.symbols.join(', ')} } from '${op.from}';`;
|
|
174
|
+
}
|
|
175
|
+
return `export * from '${op.from}';`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* True if a change kind is an UPDATE-like operation (writes to an existing
|
|
179
|
+
* file via a structural mutation). CREATE/Skip/Conflict/legacy-Update are not
|
|
180
|
+
* update-like in the v2 sense.
|
|
181
|
+
*/
|
|
182
|
+
export function isUpdateLike(type) {
|
|
183
|
+
return (type === FileChangeType.Append ||
|
|
184
|
+
type === FileChangeType.InsertAfter ||
|
|
185
|
+
type === FileChangeType.InsertBefore ||
|
|
186
|
+
type === FileChangeType.Replace ||
|
|
187
|
+
type === FileChangeType.Export);
|
|
188
|
+
}
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// Higher-level primitive evaluators
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
function evaluateEnsureImport(op, absolutePath, relativePath, existing) {
|
|
193
|
+
if (existing === null) {
|
|
194
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'ensure-import: target file does not exist', op);
|
|
195
|
+
}
|
|
196
|
+
const desiredSymbols = [...(op.symbols ?? [])].filter((s) => s.length > 0);
|
|
197
|
+
const fromSpec = op.from;
|
|
198
|
+
const importRegex = buildImportRegex(fromSpec, op.typeOnly ?? false);
|
|
199
|
+
const existingMatches = matchAll(existing, importRegex);
|
|
200
|
+
const knownSymbols = new Set();
|
|
201
|
+
let knownDefault = null;
|
|
202
|
+
let knownNamespace = null;
|
|
203
|
+
for (const m of existingMatches) {
|
|
204
|
+
const defBinding = m.groups?.['def'];
|
|
205
|
+
const nsBinding = m.groups?.['ns'];
|
|
206
|
+
const named = m.groups?.['named'];
|
|
207
|
+
if (defBinding)
|
|
208
|
+
knownDefault = defBinding;
|
|
209
|
+
if (nsBinding)
|
|
210
|
+
knownNamespace = nsBinding;
|
|
211
|
+
if (named) {
|
|
212
|
+
for (const piece of named.split(',')) {
|
|
213
|
+
const sym = piece.replace(/^\s*(?:type\s+)?/, '').replace(/\s+as\s+.*$/, '').trim();
|
|
214
|
+
if (sym.length > 0)
|
|
215
|
+
knownSymbols.add(sym);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const wantsDefault = op.defaultBinding && op.defaultBinding.length > 0
|
|
220
|
+
? op.defaultBinding
|
|
221
|
+
: null;
|
|
222
|
+
const wantsNamespace = op.namespaceBinding && op.namespaceBinding.length > 0
|
|
223
|
+
? op.namespaceBinding
|
|
224
|
+
: null;
|
|
225
|
+
const missingSymbols = desiredSymbols.filter((s) => !knownSymbols.has(s));
|
|
226
|
+
const needsDefault = wantsDefault !== null && knownDefault !== wantsDefault;
|
|
227
|
+
const needsNamespace = wantsNamespace !== null && knownNamespace !== wantsNamespace;
|
|
228
|
+
if (missingSymbols.length === 0 &&
|
|
229
|
+
!needsDefault &&
|
|
230
|
+
!needsNamespace) {
|
|
231
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'ensure-import: already present (idempotent)', op);
|
|
232
|
+
}
|
|
233
|
+
// Compose a new import line. If there is an existing line we can merge into,
|
|
234
|
+
// append the missing symbols to it; otherwise insert a fresh import at the
|
|
235
|
+
// top of the file (after the leading comment block / shebang if any).
|
|
236
|
+
if (existingMatches.length === 1 && wantsDefault === null && wantsNamespace === null) {
|
|
237
|
+
const m = existingMatches[0];
|
|
238
|
+
const namedGroup = m.groups?.['named'] ?? '';
|
|
239
|
+
const merged = mergeNamedSymbols(namedGroup, missingSymbols);
|
|
240
|
+
const newLine = m[0].replace(/\{[^}]*\}/, `{ ${merged} }`);
|
|
241
|
+
const next = existing.slice(0, m.index) + newLine + existing.slice(m.index + m[0].length);
|
|
242
|
+
return mkChange(FileChangeType.InsertAfter, absolutePath, relativePath, next, `ensure-import: merged ${missingSymbols.length} symbol(s) into existing import`, op);
|
|
243
|
+
}
|
|
244
|
+
if (existingMatches.length > 1) {
|
|
245
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `ensure-import: ${existingMatches.length} import lines reference "${fromSpec}" (ambiguous)`, op);
|
|
246
|
+
}
|
|
247
|
+
const importLine = buildImportLine(fromSpec, op.typeOnly ?? false, desiredSymbols, wantsDefault, wantsNamespace);
|
|
248
|
+
const insertAt = findImportInsertionPoint(existing);
|
|
249
|
+
const next = existing.slice(0, insertAt) + importLine + '\n' + existing.slice(insertAt);
|
|
250
|
+
return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `ensure-import: added "${fromSpec}"`, op);
|
|
251
|
+
}
|
|
252
|
+
function evaluateInsertEnumEntry(op, absolutePath, relativePath, existing) {
|
|
253
|
+
if (existing === null) {
|
|
254
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-enum-entry: target file does not exist', op);
|
|
255
|
+
}
|
|
256
|
+
const enumBlock = findEnumBlock(existing, op.enumName);
|
|
257
|
+
if (!enumBlock) {
|
|
258
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-enum-entry: enum "${op.enumName}" not found`, op);
|
|
259
|
+
}
|
|
260
|
+
if (enumBlock.duplicate) {
|
|
261
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-enum-entry: enum "${op.enumName}" appears multiple times (ambiguous)`, op);
|
|
262
|
+
}
|
|
263
|
+
const body = existing.slice(enumBlock.openIdx + 1, enumBlock.closeIdx);
|
|
264
|
+
if (new RegExp(`\\b${escapeRegex(op.entryName)}\\s*=`).test(body)) {
|
|
265
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, `insert-enum-entry: ${op.enumName}.${op.entryName} already present (idempotent)`, op);
|
|
266
|
+
}
|
|
267
|
+
const indent = detectIndent(body) || ' ';
|
|
268
|
+
const trailingTrim = body.replace(/[\s,]+$/, '');
|
|
269
|
+
const needsComma = trailingTrim.length > 0;
|
|
270
|
+
const valueLiteral = `'${op.entryValue.replace(/'/g, "\\'")}'`;
|
|
271
|
+
const insertion = `${needsComma ? ',\n' : '\n'}${indent}${op.entryName} = ${valueLiteral}`;
|
|
272
|
+
const next = existing.slice(0, enumBlock.openIdx + 1 + trailingTrim.length) +
|
|
273
|
+
insertion +
|
|
274
|
+
existing.slice(enumBlock.openIdx + 1 + trailingTrim.length);
|
|
275
|
+
return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-enum-entry: added ${op.enumName}.${op.entryName}`, op);
|
|
276
|
+
}
|
|
277
|
+
function evaluateInsertObjectEntry(op, absolutePath, relativePath, existing) {
|
|
278
|
+
if (existing === null) {
|
|
279
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-object-entry: target file does not exist', op);
|
|
280
|
+
}
|
|
281
|
+
const obj = findObjectLiteralBlock(existing, op.objectName);
|
|
282
|
+
if (!obj) {
|
|
283
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-object-entry: object "${op.objectName}" not found`, op);
|
|
284
|
+
}
|
|
285
|
+
if (obj.duplicate) {
|
|
286
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-object-entry: object "${op.objectName}" appears multiple times (ambiguous)`, op);
|
|
287
|
+
}
|
|
288
|
+
const body = existing.slice(obj.openIdx + 1, obj.closeIdx);
|
|
289
|
+
if (new RegExp(`\\b${escapeRegex(op.entryKey)}\\s*:`).test(body)) {
|
|
290
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, `insert-object-entry: ${op.objectName}.${op.entryKey} already present (idempotent)`, op);
|
|
291
|
+
}
|
|
292
|
+
const indent = detectIndent(body) || ' ';
|
|
293
|
+
const trailingTrim = body.replace(/[\s,]+$/, '');
|
|
294
|
+
const needsComma = trailingTrim.length > 0;
|
|
295
|
+
const insertion = `${needsComma ? ',\n' : '\n'}${indent}${op.entryKey}: ${op.entryValue}`;
|
|
296
|
+
const next = existing.slice(0, obj.openIdx + 1 + trailingTrim.length) +
|
|
297
|
+
insertion +
|
|
298
|
+
existing.slice(obj.openIdx + 1 + trailingTrim.length);
|
|
299
|
+
return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-object-entry: added ${op.objectName}.${op.entryKey}`, op);
|
|
300
|
+
}
|
|
301
|
+
function evaluateInsertBeforeClosingBrace(op, absolutePath, relativePath, existing) {
|
|
302
|
+
if (existing === null) {
|
|
303
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-before-closing-brace: target file does not exist', op);
|
|
304
|
+
}
|
|
305
|
+
const marker = op.ifMissing ?? op.snippet;
|
|
306
|
+
if (marker.length > 0 && existing.includes(marker)) {
|
|
307
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'insert-before-closing-brace: already present (idempotent)', op);
|
|
308
|
+
}
|
|
309
|
+
const block = findBlockByName(existing, op.containerName);
|
|
310
|
+
if (!block) {
|
|
311
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-before-closing-brace: container "${op.containerName}" not found`, op);
|
|
312
|
+
}
|
|
313
|
+
if (block.duplicate) {
|
|
314
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-before-closing-brace: container "${op.containerName}" appears multiple times (ambiguous)`, op);
|
|
315
|
+
}
|
|
316
|
+
const indent = detectIndent(existing.slice(block.openIdx + 1, block.closeIdx)) || ' ';
|
|
317
|
+
const insertion = `${indent}${op.snippet}\n`;
|
|
318
|
+
const next = existing.slice(0, block.closeIdx) + insertion + existing.slice(block.closeIdx);
|
|
319
|
+
return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-before-closing-brace: inserted into "${op.containerName}"`, op);
|
|
320
|
+
}
|
|
321
|
+
function evaluateInsertBetweenAnchors(op, absolutePath, relativePath, existing) {
|
|
322
|
+
if (existing === null) {
|
|
323
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-between-anchors: target file does not exist', op);
|
|
324
|
+
}
|
|
325
|
+
const marker = op.ifMissing ?? op.snippet;
|
|
326
|
+
if (marker.length > 0 && existing.includes(marker)) {
|
|
327
|
+
return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, 'insert-between-anchors: already present (idempotent)', op);
|
|
328
|
+
}
|
|
329
|
+
// Anchor matching is line-bounded so anchors like `// region:body`
|
|
330
|
+
// and `// region:body:end` don't trigger false-positive ambiguity.
|
|
331
|
+
const beginMatches = findLineBoundedOccurrences(existing, op.beginAnchor);
|
|
332
|
+
if (beginMatches.length === 0) {
|
|
333
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, 'insert-between-anchors: begin anchor not found', op);
|
|
334
|
+
}
|
|
335
|
+
if (beginMatches.length > 1) {
|
|
336
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, 'insert-between-anchors: begin anchor matches multiple sites (ambiguous)', op);
|
|
337
|
+
}
|
|
338
|
+
const beginIdx = beginMatches[0];
|
|
339
|
+
const endMatches = findLineBoundedOccurrences(existing, op.endAnchor, beginIdx + op.beginAnchor.length);
|
|
340
|
+
if (endMatches.length === 0) {
|
|
341
|
+
return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, 'insert-between-anchors: end anchor not found after begin anchor', op);
|
|
342
|
+
}
|
|
343
|
+
const endIdx = endMatches[0];
|
|
344
|
+
const insertionPoint = beginIdx + op.beginAnchor.length;
|
|
345
|
+
const between = existing.slice(insertionPoint, endIdx);
|
|
346
|
+
const sep = between.endsWith('\n') ? '' : '\n';
|
|
347
|
+
const next = existing.slice(0, endIdx) + sep + op.snippet + '\n' + existing.slice(endIdx);
|
|
348
|
+
return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-between-anchors: inserted between anchors`, op);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Return indices where `needle` appears in `haystack` AS A FULL LINE-
|
|
352
|
+
* BOUNDED OCCURRENCE: the character immediately after must be EOL or EOF.
|
|
353
|
+
* The character before must be start-of-file or a newline.
|
|
354
|
+
*/
|
|
355
|
+
function findLineBoundedOccurrences(haystack, needle, startAt = 0) {
|
|
356
|
+
const out = [];
|
|
357
|
+
if (needle.length === 0)
|
|
358
|
+
return out;
|
|
359
|
+
let from = startAt;
|
|
360
|
+
while (true) {
|
|
361
|
+
const i = haystack.indexOf(needle, from);
|
|
362
|
+
if (i < 0)
|
|
363
|
+
return out;
|
|
364
|
+
const afterChar = haystack[i + needle.length];
|
|
365
|
+
const beforeChar = i === 0 ? '\n' : haystack[i - 1];
|
|
366
|
+
const beforeOk = beforeChar === '\n' || beforeChar === '\r' || i === 0 ||
|
|
367
|
+
// Tolerate leading whitespace on the anchor line.
|
|
368
|
+
/[ \t]/.test(beforeChar ?? '');
|
|
369
|
+
const afterOk = afterChar === undefined || afterChar === '\n' || afterChar === '\r';
|
|
370
|
+
if (beforeOk && afterOk)
|
|
371
|
+
out.push(i);
|
|
372
|
+
from = i + needle.length;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// ─── Helpers for primitive evaluators ───────────────────────────────────────
|
|
376
|
+
function escapeRegex(s) {
|
|
377
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
378
|
+
}
|
|
379
|
+
function buildImportRegex(fromSpec, typeOnly) {
|
|
380
|
+
const fromEsc = escapeRegex(fromSpec);
|
|
381
|
+
// Match: import [type] [<default>,] [* as <ns>,] [{ <named> },] from '<from>'
|
|
382
|
+
const prefix = typeOnly ? 'import\\s+type\\s+' : 'import(?:\\s+type)?\\s+';
|
|
383
|
+
return new RegExp(`${prefix}(?:(?<def>[A-Za-z_$][A-Za-z0-9_$]*)\\s*,?\\s*)?(?:\\*\\s+as\\s+(?<ns>[A-Za-z_$][A-Za-z0-9_$]*)\\s*,?\\s*)?(?:\\{(?<named>[^}]*)\\}\\s*)?from\\s+['"]${fromEsc}['"];?`, 'g');
|
|
384
|
+
}
|
|
385
|
+
function matchAll(input, re) {
|
|
386
|
+
const out = [];
|
|
387
|
+
re.lastIndex = 0;
|
|
388
|
+
let m;
|
|
389
|
+
while ((m = re.exec(input)) !== null) {
|
|
390
|
+
out.push(m);
|
|
391
|
+
if (m.index === re.lastIndex)
|
|
392
|
+
re.lastIndex += 1;
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
function mergeNamedSymbols(existingNamed, additions) {
|
|
397
|
+
const seen = new Set();
|
|
398
|
+
const out = [];
|
|
399
|
+
const push = (raw) => {
|
|
400
|
+
const trimmed = raw.trim();
|
|
401
|
+
if (!trimmed)
|
|
402
|
+
return;
|
|
403
|
+
const key = trimmed.replace(/^type\s+/, '').replace(/\s+as\s+.*$/, '');
|
|
404
|
+
if (seen.has(key))
|
|
405
|
+
return;
|
|
406
|
+
seen.add(key);
|
|
407
|
+
out.push(trimmed);
|
|
408
|
+
};
|
|
409
|
+
for (const piece of existingNamed.split(','))
|
|
410
|
+
push(piece);
|
|
411
|
+
for (const sym of additions)
|
|
412
|
+
push(sym);
|
|
413
|
+
return out.join(', ');
|
|
414
|
+
}
|
|
415
|
+
function buildImportLine(fromSpec, typeOnly, symbols, defaultBinding, namespaceBinding) {
|
|
416
|
+
const keyword = typeOnly ? 'import type' : 'import';
|
|
417
|
+
const parts = [];
|
|
418
|
+
if (defaultBinding)
|
|
419
|
+
parts.push(defaultBinding);
|
|
420
|
+
if (namespaceBinding)
|
|
421
|
+
parts.push(`* as ${namespaceBinding}`);
|
|
422
|
+
if (symbols.length > 0)
|
|
423
|
+
parts.push(`{ ${symbols.join(', ')} }`);
|
|
424
|
+
if (parts.length === 0) {
|
|
425
|
+
return `${keyword} '${fromSpec}';`;
|
|
426
|
+
}
|
|
427
|
+
return `${keyword} ${parts.join(', ')} from '${fromSpec}';`;
|
|
428
|
+
}
|
|
429
|
+
function findImportInsertionPoint(source) {
|
|
430
|
+
// Skip shebang + leading line comments + leading block comments.
|
|
431
|
+
let i = 0;
|
|
432
|
+
if (source.startsWith('#!')) {
|
|
433
|
+
const nl = source.indexOf('\n');
|
|
434
|
+
if (nl < 0)
|
|
435
|
+
return source.length;
|
|
436
|
+
i = nl + 1;
|
|
437
|
+
}
|
|
438
|
+
while (i < source.length) {
|
|
439
|
+
// Skip whitespace lines.
|
|
440
|
+
if (source[i] === '\n' || source[i] === '\r' || source[i] === '\t' || source[i] === ' ') {
|
|
441
|
+
i += 1;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (source.startsWith('//', i)) {
|
|
445
|
+
const nl = source.indexOf('\n', i);
|
|
446
|
+
i = nl < 0 ? source.length : nl + 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (source.startsWith('/*', i)) {
|
|
450
|
+
const end = source.indexOf('*/', i + 2);
|
|
451
|
+
if (end < 0)
|
|
452
|
+
return source.length;
|
|
453
|
+
i = end + 2;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return i;
|
|
459
|
+
}
|
|
460
|
+
function findEnumBlock(source, enumName) {
|
|
461
|
+
const re = new RegExp(`\\benum\\s+${escapeRegex(enumName)}\\s*\\{`, 'g');
|
|
462
|
+
return findBraceBlock(source, re);
|
|
463
|
+
}
|
|
464
|
+
function findObjectLiteralBlock(source, objectName) {
|
|
465
|
+
const re = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegex(objectName)}\\b[^=]*=\\s*\\{`, 'g');
|
|
466
|
+
return findBraceBlock(source, re);
|
|
467
|
+
}
|
|
468
|
+
function findBlockByName(source, name) {
|
|
469
|
+
// Matches `class Name {`, `interface Name {`, `enum Name {`, `namespace Name {`.
|
|
470
|
+
const re = new RegExp(`\\b(?:class|interface|enum|namespace|module)\\s+${escapeRegex(name)}\\b[^{]*\\{`, 'g');
|
|
471
|
+
return findBraceBlock(source, re);
|
|
472
|
+
}
|
|
473
|
+
function findBraceBlock(source, headRegex) {
|
|
474
|
+
headRegex.lastIndex = 0;
|
|
475
|
+
const first = headRegex.exec(source);
|
|
476
|
+
if (!first)
|
|
477
|
+
return null;
|
|
478
|
+
const openIdx = first.index + first[0].length - 1;
|
|
479
|
+
const second = headRegex.exec(source);
|
|
480
|
+
const duplicate = second !== null;
|
|
481
|
+
const closeIdx = findMatchingClose(source, openIdx);
|
|
482
|
+
if (closeIdx < 0)
|
|
483
|
+
return null;
|
|
484
|
+
return { openIdx, closeIdx, duplicate };
|
|
485
|
+
}
|
|
486
|
+
function findMatchingClose(source, openBraceIdx) {
|
|
487
|
+
let depth = 0;
|
|
488
|
+
let i = openBraceIdx;
|
|
489
|
+
while (i < source.length) {
|
|
490
|
+
const ch = source[i];
|
|
491
|
+
if (ch === '{')
|
|
492
|
+
depth += 1;
|
|
493
|
+
else if (ch === '}') {
|
|
494
|
+
depth -= 1;
|
|
495
|
+
if (depth === 0)
|
|
496
|
+
return i;
|
|
497
|
+
}
|
|
498
|
+
i += 1;
|
|
499
|
+
}
|
|
500
|
+
return -1;
|
|
501
|
+
}
|
|
502
|
+
function detectIndent(body) {
|
|
503
|
+
const match = body.match(/^([ \t]+)\S/m);
|
|
504
|
+
if (!match)
|
|
505
|
+
return null;
|
|
506
|
+
return match[1] ?? null;
|
|
507
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
2
|
+
import type { IGenerationPlan } from './generation-plan.js';
|
|
3
|
+
import type { IPlannedOperation } from './planned-change.js';
|
|
4
|
+
/** v1 schema marker — kept for legacy CREATE-only plans. */
|
|
5
|
+
export declare const SAVED_PLAN_SCHEMA_V1 = "sharkcraft.plan/v1";
|
|
6
|
+
/** v2 schema marker — emitted when any change carries a v2 operation. */
|
|
7
|
+
export declare const SAVED_PLAN_SCHEMA_V2 = "sharkcraft.plan/v2";
|
|
8
|
+
/** Default exported alias — points at v1 for backward-compat with consumers. */
|
|
9
|
+
export declare const SAVED_PLAN_SCHEMA = "sharkcraft.plan/v1";
|
|
10
|
+
export type SavedPlanSchema = typeof SAVED_PLAN_SCHEMA_V1 | typeof SAVED_PLAN_SCHEMA_V2;
|
|
11
|
+
export interface ISavedPlanExpectedChange {
|
|
12
|
+
type: string;
|
|
13
|
+
relativePath: string;
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
/**
|
|
16
|
+
* v2-only — the operation intent that produced this change. Present iff
|
|
17
|
+
* the schema is `sharkcraft.plan/v2`. Tampering with this field invalidates
|
|
18
|
+
* the HMAC signature (canonical JSON includes the whole `expectedChanges`).
|
|
19
|
+
*/
|
|
20
|
+
operation?: IPlannedOperation;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Folder operations carried by a saved plan.
|
|
24
|
+
*
|
|
25
|
+
* Folder ops are NOT files; they live alongside `expectedChanges`. The HMAC
|
|
26
|
+
* signature naturally covers them because the canonical-JSON encoding sorts
|
|
27
|
+
* keys and includes any present field. Apply executes folder ops via
|
|
28
|
+
* `applyFolderOps()` after explicit `--allow-folder-ops` (and
|
|
29
|
+
* `--allow-delete-folder` for deletes).
|
|
30
|
+
*/
|
|
31
|
+
export interface ISavedPlanFolderOp {
|
|
32
|
+
kind: 'rename-folder' | 'delete-folder';
|
|
33
|
+
targetPath: string;
|
|
34
|
+
newPath?: string;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ISavedPlan {
|
|
38
|
+
/** Schema marker for forward-compat. */
|
|
39
|
+
schema: SavedPlanSchema;
|
|
40
|
+
templateId: string;
|
|
41
|
+
/** Primary kebab-case name passed to the template. Optional. */
|
|
42
|
+
name?: string;
|
|
43
|
+
variables: Record<string, string>;
|
|
44
|
+
/** Absolute path of the project root the plan was created against. */
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
/** ISO timestamp of when the plan was saved. */
|
|
47
|
+
createdAt: string;
|
|
48
|
+
/**
|
|
49
|
+
* Optional summary of the plan's expected changes at save time. Used as a
|
|
50
|
+
* sanity check during `shrk apply`; if the live plan diverges, the CLI
|
|
51
|
+
* surfaces a warning before writing.
|
|
52
|
+
*/
|
|
53
|
+
expectedChanges?: ReadonlyArray<ISavedPlanExpectedChange>;
|
|
54
|
+
/**
|
|
55
|
+
* Folder operations carried alongside file changes. The HMAC
|
|
56
|
+
* signature covers this field via canonical-JSON. Apply executes these
|
|
57
|
+
* via `applyFolderOps()` only when explicit `--allow-folder-ops` (and
|
|
58
|
+
* `--allow-delete-folder` for deletes) is passed.
|
|
59
|
+
*/
|
|
60
|
+
folderOps?: ReadonlyArray<ISavedPlanFolderOp>;
|
|
61
|
+
/** Optional free-form notes from whoever saved the plan. */
|
|
62
|
+
note?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Optional HMAC signature. Excluded from the canonical JSON that's signed.
|
|
65
|
+
*/
|
|
66
|
+
signature?: {
|
|
67
|
+
algo: 'sha256';
|
|
68
|
+
hmac: string;
|
|
69
|
+
signedAt: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface BuildSavedPlanInput {
|
|
73
|
+
templateId: string;
|
|
74
|
+
name?: string;
|
|
75
|
+
variables: Record<string, string>;
|
|
76
|
+
projectRoot: string;
|
|
77
|
+
plan: IGenerationPlan;
|
|
78
|
+
note?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Folder operations to carry alongside the file plan. Tags the plan
|
|
81
|
+
* as v2.
|
|
82
|
+
*/
|
|
83
|
+
folderOps?: readonly ISavedPlanFolderOp[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build a saved plan. If any change in the plan carries a v2 operation, the
|
|
87
|
+
* resulting plan is tagged `sharkcraft.plan/v2`; otherwise v1.
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildSavedPlan(input: BuildSavedPlanInput): ISavedPlan;
|
|
90
|
+
export declare function savePlanToFile(plan: ISavedPlan, filePath: string): Result<void, AppError>;
|
|
91
|
+
export declare function readPlanFromFile(filePath: string): Result<ISavedPlan, AppError>;
|
|
92
|
+
export interface IPlanDiff {
|
|
93
|
+
relativePath: string;
|
|
94
|
+
/** "added" | "removed" | "type-changed" | "size-changed" | "operation-changed" */
|
|
95
|
+
kind: 'added' | 'removed' | 'type-changed' | 'size-changed' | 'operation-changed';
|
|
96
|
+
detail?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Compare the saved plan's expected changes with a freshly-computed plan's
|
|
100
|
+
* changes. Returns an empty array when they match. v2 plans additionally
|
|
101
|
+
* detect operation-intent drift (e.g. signed `append` became `replace` in
|
|
102
|
+
* the template).
|
|
103
|
+
*/
|
|
104
|
+
export declare function diffPlanChanges(saved: ISavedPlan, fresh: IGenerationPlan): IPlanDiff[];
|
|
105
|
+
/**
|
|
106
|
+
* Folder-op divergence. Returns an empty array when saved and live
|
|
107
|
+
* folder ops match (by kind+targetPath+newPath).
|
|
108
|
+
*/
|
|
109
|
+
export declare function diffPlanFolderOps(saved: ISavedPlan, liveFolderOps: readonly ISavedPlanFolderOp[]): IPlanDiff[];
|
|
110
|
+
//# sourceMappingURL=saved-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saved-plan.d.ts","sourceRoot":"","sources":["../src/saved-plan.ts"],"names":[],"mappings":"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"}
|