@shrkcrft/generator 0.1.0-alpha.13 → 0.1.0-alpha.14

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAW5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,gEAAgE;IAChE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAmGf"}
1
+ {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAW5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,gEAAgE;IAChE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CA8Gf"}
package/dist/dry-run.js CHANGED
@@ -30,6 +30,12 @@ export function planGeneration(template, request) {
30
30
  }
31
31
  const rendered = renderTemplate(template, validation.resolved);
32
32
  const changes = [];
33
+ // Per-file content overlay so that MULTIPLE changes (or a files() create
34
+ // followed by a changes() op) targeting the SAME file COMPOSE. Without
35
+ // this every change is evaluated against the original on-disk bytes and
36
+ // the writer's last same-file write clobbers the earlier ones. Keyed by
37
+ // absolute path → the cumulative content after the prior change(s).
38
+ const overlay = new Map();
33
39
  // 1) Legacy CREATE-only files() output — unchanged behaviour.
34
40
  for (const file of rendered.files) {
35
41
  let safe;
@@ -59,14 +65,16 @@ export function planGeneration(template, request) {
59
65
  // unreadable existing file — treat as conflict
60
66
  }
61
67
  const decision = decideForExisting(file.overwrite ? OverwriteStrategy.Overwrite : overwriteStrategy, existing, file.content);
68
+ const contents = decision.type === FileChangeType.Skip ? existing : file.content;
62
69
  changes.push({
63
70
  type: decision.type,
64
71
  absolutePath,
65
72
  relativePath: relPath,
66
- contents: decision.type === FileChangeType.Skip ? existing : file.content,
73
+ contents,
67
74
  reason: decision.reason,
68
75
  sizeBytes: Buffer.byteLength(file.content, 'utf8'),
69
76
  });
77
+ overlay.set(absolutePath, contents);
70
78
  }
71
79
  else {
72
80
  changes.push({
@@ -77,11 +85,13 @@ export function planGeneration(template, request) {
77
85
  reason: 'New file (does not exist)',
78
86
  sizeBytes: Buffer.byteLength(file.content, 'utf8'),
79
87
  });
88
+ overlay.set(absolutePath, file.content);
80
89
  }
81
90
  }
82
- // 2) v2 planned changes — evaluate against live filesystem.
91
+ // 2) v2 planned changes — evaluate against the live filesystem, threaded
92
+ // through the overlay so successive changes to one file compose.
83
93
  for (const tplChange of rendered.changes) {
84
- const evaluated = planOne(tplChange, request.projectRoot);
94
+ const evaluated = planOne(tplChange, request.projectRoot, overlay);
85
95
  changes.push(evaluated);
86
96
  }
87
97
  const { hasConflicts } = summarizeConflicts(changes);
@@ -98,7 +108,7 @@ export function planGeneration(template, request) {
98
108
  safe: !hasConflicts && changes.length > 0,
99
109
  };
100
110
  }
101
- function planOne(tplChange, projectRoot) {
111
+ function planOne(tplChange, projectRoot, overlay) {
102
112
  const op = tplChange.operation;
103
113
  let safe;
104
114
  try {
@@ -121,15 +131,23 @@ function planOne(tplChange, projectRoot) {
121
131
  targetPath: tplChange.targetPath,
122
132
  operation: op,
123
133
  };
124
- const existing = existsSync(safe.absolutePath)
125
- ? readFileSafe(safe.absolutePath)
126
- : null;
127
- return evaluatePlannedChange({
134
+ // Prefer the overlay (cumulative result of prior same-file changes) over
135
+ // the on-disk bytes so successive ops on one file compose deterministically.
136
+ const existing = overlay?.has(safe.absolutePath)
137
+ ? (overlay.get(safe.absolutePath) ?? null)
138
+ : existsSync(safe.absolutePath)
139
+ ? readFileSafe(safe.absolutePath)
140
+ : null;
141
+ const result = evaluatePlannedChange({
128
142
  change,
129
143
  absolutePath: safe.absolutePath,
130
144
  relativePath: safe.relativePath,
131
145
  existing,
132
146
  });
147
+ // Record the cumulative content (Skip/Conflict carry the unchanged bytes,
148
+ // which is exactly what a later op on the same file should see).
149
+ overlay?.set(safe.absolutePath, result.contents);
150
+ return result;
133
151
  }
134
152
  function readFileSafe(absolutePath) {
135
153
  try {
@@ -170,6 +188,8 @@ function previewContentForOperation(op) {
170
188
  return `${op.enumName}.${op.entryName} = '${op.entryValue}'`;
171
189
  case 'insert-object-entry':
172
190
  return `${op.objectName}.${op.entryKey}: ${op.entryValue}`;
191
+ case 'insert-array-entry':
192
+ return `${op.arrayName} ⟵ ${op.entryValue}`;
173
193
  case 'insert-before-closing-brace':
174
194
  return op.snippet;
175
195
  case 'insert-between-anchors':
@@ -21,7 +21,7 @@
21
21
  * - MCP stays read-only — this module is pure logic.
22
22
  */
23
23
  import { FileChangeType, type IFileChange } from './file-change.js';
24
- export type PlannedOperationKind = 'create' | 'append' | 'insert-after' | 'insert-before' | 'replace' | 'export' | 'ensure-import' | 'insert-enum-entry' | 'insert-object-entry' | 'insert-before-closing-brace' | 'insert-between-anchors';
24
+ export type PlannedOperationKind = 'create' | 'append' | 'insert-after' | 'insert-before' | 'replace' | 'export' | 'ensure-import' | 'insert-enum-entry' | 'insert-object-entry' | 'insert-array-entry' | 'insert-before-closing-brace' | 'insert-between-anchors';
25
25
  interface ICreateOperation {
26
26
  kind: 'create';
27
27
  content: string;
@@ -123,6 +123,20 @@ interface IInsertObjectEntryOperation {
123
123
  shorthand?: boolean;
124
124
  description?: string;
125
125
  }
126
+ interface IInsertArrayEntryOperation {
127
+ kind: 'insert-array-entry';
128
+ /**
129
+ * Array identifier — a `const`/`let`/`var` bound to an array literal,
130
+ * e.g. `editorScopeEntries` or `DEFAULT_PANELS`. The element is inserted
131
+ * before the array's matching closing bracket.
132
+ */
133
+ arrayName: string;
134
+ /** Element source text to add (already source-formatted, no trailing comma). */
135
+ entryValue: string;
136
+ /** Optional idempotency marker (default = `entryValue`). */
137
+ ifMissing?: string;
138
+ description?: string;
139
+ }
126
140
  interface IInsertBeforeClosingBraceOperation {
127
141
  kind: 'insert-before-closing-brace';
128
142
  /** Container identifier, e.g. an interface/class/enum name. */
@@ -142,7 +156,7 @@ interface IInsertBetweenAnchorsOperation {
142
156
  ifMissing?: string;
143
157
  description?: string;
144
158
  }
145
- export type IPlannedOperation = ICreateOperation | IAppendOperation | IInsertAfterOperation | IInsertBeforeOperation | IReplaceOperation | IExportOperation | IEnsureImportOperation | IInsertEnumEntryOperation | IInsertObjectEntryOperation | IInsertBeforeClosingBraceOperation | IInsertBetweenAnchorsOperation;
159
+ export type IPlannedOperation = ICreateOperation | IAppendOperation | IInsertAfterOperation | IInsertBeforeOperation | IReplaceOperation | IExportOperation | IEnsureImportOperation | IInsertEnumEntryOperation | IInsertObjectEntryOperation | IInsertArrayEntryOperation | IInsertBeforeClosingBraceOperation | IInsertBetweenAnchorsOperation;
146
160
  export interface IPlannedChange {
147
161
  /** Final file path relative to project root. */
148
162
  targetPath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"planned-change.d.ts","sourceRoot":"","sources":["../src/planned-change.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"planned-change.d.ts","sourceRoot":"","sources":["../src/planned-change.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,8 @@ export function evaluatePlannedChange(input) {
43
43
  return evaluateInsertEnumEntry(op, absolutePath, relativePath, existing);
44
44
  case 'insert-object-entry':
45
45
  return evaluateInsertObjectEntry(op, absolutePath, relativePath, existing);
46
+ case 'insert-array-entry':
47
+ return evaluateInsertArrayEntry(op, absolutePath, relativePath, existing);
46
48
  case 'insert-before-closing-brace':
47
49
  return evaluateInsertBeforeClosingBrace(op, absolutePath, relativePath, existing);
48
50
  case 'insert-between-anchors':
@@ -298,6 +300,33 @@ function evaluateInsertObjectEntry(op, absolutePath, relativePath, existing) {
298
300
  existing.slice(obj.openIdx + 1 + trailingTrim.length);
299
301
  return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-object-entry: added ${op.objectName}.${op.entryKey}`, op);
300
302
  }
303
+ function evaluateInsertArrayEntry(op, absolutePath, relativePath, existing) {
304
+ if (existing === null) {
305
+ return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-array-entry: target file does not exist', op);
306
+ }
307
+ const arr = findArrayLiteralBlock(existing, op.arrayName);
308
+ if (!arr) {
309
+ return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-array-entry: array "${op.arrayName}" not found`, op);
310
+ }
311
+ if (arr.duplicate) {
312
+ return mkChange(FileChangeType.Conflict, absolutePath, relativePath, existing, `insert-array-entry: array "${op.arrayName}" appears multiple times (ambiguous)`, op);
313
+ }
314
+ // Idempotency: skip when the element (or its caller-supplied marker) is
315
+ // already present anywhere inside the array body.
316
+ const body = existing.slice(arr.openIdx + 1, arr.closeIdx);
317
+ const marker = op.ifMissing ?? op.entryValue;
318
+ if (marker.length > 0 && body.includes(marker)) {
319
+ return mkChange(FileChangeType.Skip, absolutePath, relativePath, existing, `insert-array-entry: "${op.arrayName}" already contains entry (idempotent)`, op);
320
+ }
321
+ const indent = detectIndent(body) || ' ';
322
+ const trailingTrim = body.replace(/[\s,]+$/, '');
323
+ const needsComma = trailingTrim.length > 0;
324
+ const insertion = `${needsComma ? ',\n' : '\n'}${indent}${op.entryValue}`;
325
+ const next = existing.slice(0, arr.openIdx + 1 + trailingTrim.length) +
326
+ insertion +
327
+ existing.slice(arr.openIdx + 1 + trailingTrim.length);
328
+ return mkChange(FileChangeType.InsertBefore, absolutePath, relativePath, next, `insert-array-entry: added entry to ${op.arrayName}`, op);
329
+ }
301
330
  function evaluateInsertBeforeClosingBrace(op, absolutePath, relativePath, existing) {
302
331
  if (existing === null) {
303
332
  return mkChange(FileChangeType.Conflict, absolutePath, relativePath, '', 'insert-before-closing-brace: target file does not exist', op);
@@ -465,6 +494,39 @@ function findObjectLiteralBlock(source, objectName) {
465
494
  const re = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegex(objectName)}\\b[^=]*=\\s*\\{`, 'g');
466
495
  return findBraceBlock(source, re);
467
496
  }
497
+ function findArrayLiteralBlock(source, arrayName) {
498
+ const re = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegex(arrayName)}\\b[^=]*=\\s*\\[`, 'g');
499
+ return findBracketBlock(source, re);
500
+ }
501
+ function findBracketBlock(source, headRegex) {
502
+ headRegex.lastIndex = 0;
503
+ const first = headRegex.exec(source);
504
+ if (!first)
505
+ return null;
506
+ const openIdx = first.index + first[0].length - 1;
507
+ const second = headRegex.exec(source);
508
+ const duplicate = second !== null;
509
+ const closeIdx = findMatchingCloseBracket(source, openIdx);
510
+ if (closeIdx < 0)
511
+ return null;
512
+ return { openIdx, closeIdx, duplicate };
513
+ }
514
+ function findMatchingCloseBracket(source, openBracketIdx) {
515
+ let depth = 0;
516
+ let i = openBracketIdx;
517
+ while (i < source.length) {
518
+ const ch = source[i];
519
+ if (ch === '[')
520
+ depth += 1;
521
+ else if (ch === ']') {
522
+ depth -= 1;
523
+ if (depth === 0)
524
+ return i;
525
+ }
526
+ i += 1;
527
+ }
528
+ return -1;
529
+ }
468
530
  function findBlockByName(source, name) {
469
531
  // Matches `class Name {`, `interface Name {`, `enum Name {`, `namespace Name {`.
470
532
  const re = new RegExp(`\\b(?:class|interface|enum|namespace|module)\\s+${escapeRegex(name)}\\b[^{]*\\{`, 'g');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/generator",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "SharkCraft plan-first generator: GenerationPlan, FileChange, dry-run, safe writes.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -43,10 +43,10 @@
43
43
  "typecheck": "tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  "dependencies": {
46
- "@shrkcrft/core": "^0.1.0-alpha.13",
47
- "@shrkcrft/templates": "^0.1.0-alpha.13",
48
- "@shrkcrft/rules": "^0.1.0-alpha.13",
49
- "@shrkcrft/paths": "^0.1.0-alpha.13"
46
+ "@shrkcrft/core": "^0.1.0-alpha.14",
47
+ "@shrkcrft/templates": "^0.1.0-alpha.14",
48
+ "@shrkcrft/rules": "^0.1.0-alpha.14",
49
+ "@shrkcrft/paths": "^0.1.0-alpha.14"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"