@shrkcrft/generator 0.1.0-alpha.16 → 0.1.0-alpha.18

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":"folder-apply.d.ts","sourceRoot":"","sources":["../src/folder-apply.ts"],"names":[],"mappings":"AAcA,OAAO,EAAuB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,cAAc,EAAE,EAC9B,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAsGtB"}
1
+ {"version":3,"file":"folder-apply.d.ts","sourceRoot":"","sources":["../src/folder-apply.ts"],"names":[],"mappings":"AAeA,OAAO,EAAuB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,cAAc,EAAE,EAC9B,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAwHtB"}
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { existsSync, renameSync, rmSync } from 'node:fs';
14
14
  import * as nodePath from 'node:path';
15
+ import { safeResolveTargetPath } from '@shrkcrft/core';
15
16
  import { checkFolderOpSafety, FolderOpSafety } from "./folder-safety.js";
16
17
  function resolveAbs(projectRoot, p) {
17
18
  return nodePath.isAbsolute(p) ? p : nodePath.resolve(projectRoot, p);
@@ -60,7 +61,26 @@ export function applyFolderOps(ops, options) {
60
61
  });
61
62
  continue;
62
63
  }
63
- if (existsSync(absNewPath)) {
64
+ // The rename DESTINATION is never validated by checkFolderOpSafety (which
65
+ // only checks targetPath), and resolveAbs even allows absolute newPaths —
66
+ // so `newPath: '../sibling'` or '/etc/x' would move the folder OUTSIDE the
67
+ // project root. Enforce containment through the same chokepoint as writes.
68
+ let safeNew;
69
+ try {
70
+ safeNew = safeResolveTargetPath(op.newPath, options.projectRoot);
71
+ }
72
+ catch (e) {
73
+ const pathErr = e;
74
+ rejected.push({
75
+ ...baseResult,
76
+ applied: false,
77
+ safety: FolderOpSafety.Unsafe,
78
+ reason: `rename-folder destination "${op.newPath}" is outside the project root (${pathErr.code}).`,
79
+ });
80
+ continue;
81
+ }
82
+ const safeAbsNewPath = safeNew.absolutePath;
83
+ if (existsSync(safeAbsNewPath)) {
64
84
  rejected.push({
65
85
  ...baseResult,
66
86
  applied: false,
@@ -74,7 +94,7 @@ export function applyFolderOps(ops, options) {
74
94
  continue;
75
95
  }
76
96
  try {
77
- renameSync(absTarget, absNewPath);
97
+ renameSync(absTarget, safeAbsNewPath);
78
98
  applied.push({ ...baseResult, applied: true });
79
99
  }
80
100
  catch (e) {
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * from './planned-change.js';
12
12
  export * from './folder-safety.js';
13
13
  export * from './folder-apply.js';
14
14
  export * from './synthetic-plan.js';
15
+ export * from './package-delegate-plan.js';
15
16
  export * from './spec/index.js';
16
17
  export * from './grounding/index.js';
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -12,5 +12,6 @@ export * from "./planned-change.js";
12
12
  export * from "./folder-safety.js";
13
13
  export * from "./folder-apply.js";
14
14
  export * from "./synthetic-plan.js";
15
+ export * from "./package-delegate-plan.js";
15
16
  export * from "./spec/index.js";
16
17
  export * from "./grounding/index.js";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Package a delegate worker's raw edit into a SIGNED-ready synthetic plan.
3
+ *
4
+ * This is the deterministic chokepoint between a stochastic worker and the
5
+ * write path. It:
6
+ * 1. drops ops whose `kind` is not in the recipe's `allowedOps` (reported,
7
+ * never applied);
8
+ * 2. validates every remaining op's fields against the real operation union —
9
+ * a malformed op refuses the whole package (the worker must retry);
10
+ * 3. evaluates the ops against the live file system via the SAME
11
+ * `evaluateSavedPlanInPlace` apply uses, so anchor-not-found / ambiguous /
12
+ * file-missing all surface as conflicts BEFORE anything is signed;
13
+ * 4. builds an `ISavedPlan` (templateId `__delegate/<recipe>`) the caller
14
+ * signs + applies through the unmodified apply pipeline.
15
+ *
16
+ * No model, no network. The raw-op input type is declared locally so the
17
+ * generator layer carries no dependency on `@shrkcrft/ai`.
18
+ */
19
+ import { type AppError, type Result } from '@shrkcrft/core';
20
+ import type { IGenerationPlan } from './generation-plan.js';
21
+ import type { ISavedPlan } from './saved-plan.js';
22
+ export declare const DELEGATE_TEMPLATE_PREFIX = "__delegate/";
23
+ /** A raw operation as parsed from a worker (structurally `IDelegateRawOp`). */
24
+ export interface IDelegateOpInput {
25
+ targetPath: string;
26
+ operation: {
27
+ kind: string;
28
+ } & Record<string, unknown>;
29
+ }
30
+ export interface IPackageDelegateInput {
31
+ ops: readonly IDelegateOpInput[];
32
+ /** `IPlannedOperation` kinds the recipe permits; others are dropped. */
33
+ allowedOps: readonly string[];
34
+ /** Recipe id — becomes the synthetic templateId `__delegate/<id>`. */
35
+ recipeId: string;
36
+ projectRoot: string;
37
+ }
38
+ export interface IDroppedOp {
39
+ kind: string;
40
+ targetPath: string;
41
+ reason: string;
42
+ }
43
+ export interface IPackageDelegateResult {
44
+ /** Conflict-free, ready to sign. Present only when `ready`. */
45
+ plan?: ISavedPlan;
46
+ /** The evaluated generation plan (changes + conflicts) — always present. */
47
+ generation: IGenerationPlan;
48
+ /** Ops whose kind was not in `allowedOps`. */
49
+ droppedOps: readonly IDroppedOp[];
50
+ /** True when no conflicts — the plan is built and ready to sign + apply. */
51
+ ready: boolean;
52
+ }
53
+ export declare function packageDelegatePlan(input: IPackageDelegateInput): Result<IPackageDelegateResult, AppError>;
54
+ //# sourceMappingURL=package-delegate-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-delegate-plan.d.ts","sourceRoot":"","sources":["../src/package-delegate-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAmBlD,eAAO,MAAM,wBAAwB,gBAAgB,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjC,wEAAwE;IACxE,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,4EAA4E;IAC5E,UAAU,EAAE,eAAe,CAAC;IAC5B,8CAA8C;IAC9C,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAClC,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,qBAAqB,GAC3B,MAAM,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAoE1C"}
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Package a delegate worker's raw edit into a SIGNED-ready synthetic plan.
3
+ *
4
+ * This is the deterministic chokepoint between a stochastic worker and the
5
+ * write path. It:
6
+ * 1. drops ops whose `kind` is not in the recipe's `allowedOps` (reported,
7
+ * never applied);
8
+ * 2. validates every remaining op's fields against the real operation union —
9
+ * a malformed op refuses the whole package (the worker must retry);
10
+ * 3. evaluates the ops against the live file system via the SAME
11
+ * `evaluateSavedPlanInPlace` apply uses, so anchor-not-found / ambiguous /
12
+ * file-missing all surface as conflicts BEFORE anything is signed;
13
+ * 4. builds an `ISavedPlan` (templateId `__delegate/<recipe>`) the caller
14
+ * signs + applies through the unmodified apply pipeline.
15
+ *
16
+ * No model, no network. The raw-op input type is declared locally so the
17
+ * generator layer carries no dependency on `@shrkcrft/ai`.
18
+ */
19
+ import { err, ok, AppErrorImpl, ERROR_CODES } from '@shrkcrft/core';
20
+ import { buildSavedPlan } from "./saved-plan.js";
21
+ import { evaluateSavedPlanInPlace } from "./synthetic-plan.js";
22
+ export const DELEGATE_TEMPLATE_PREFIX = '__delegate/';
23
+ export function packageDelegatePlan(input) {
24
+ const allowed = new Set(input.allowedOps);
25
+ const droppedOps = [];
26
+ const expectedChanges = [];
27
+ for (const raw of input.ops) {
28
+ const kind = raw.operation.kind;
29
+ if (!allowed.has(kind)) {
30
+ droppedOps.push({
31
+ kind,
32
+ targetPath: raw.targetPath,
33
+ reason: `op kind "${kind}" is not in the recipe's allowedOps`,
34
+ });
35
+ continue;
36
+ }
37
+ const coerced = coerceOperation(raw.operation);
38
+ if (!coerced.ok) {
39
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `delegate op for ${raw.targetPath}: ${coerced.error}`));
40
+ }
41
+ expectedChanges.push({
42
+ type: 'pending',
43
+ relativePath: raw.targetPath,
44
+ sizeBytes: 0,
45
+ operation: coerced.value,
46
+ });
47
+ }
48
+ if (expectedChanges.length === 0) {
49
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, droppedOps.length > 0
50
+ ? `delegate edit had ${droppedOps.length} op(s), all of disallowed kinds`
51
+ : 'delegate edit contained no operations'));
52
+ }
53
+ // Evaluate against the live FS through the SAME path apply uses — conflicts
54
+ // (ambiguous anchor, file missing, replace 0/>N) surface here, before signing.
55
+ const draft = {
56
+ schema: 'sharkcraft.plan/v2',
57
+ templateId: `${DELEGATE_TEMPLATE_PREFIX}${input.recipeId}`,
58
+ variables: {},
59
+ projectRoot: input.projectRoot,
60
+ createdAt: new Date().toISOString(),
61
+ expectedChanges,
62
+ };
63
+ const generation = evaluateSavedPlanInPlace(draft, input.projectRoot);
64
+ if (generation.hasConflicts) {
65
+ return ok({ generation, droppedOps, ready: false });
66
+ }
67
+ const plan = buildSavedPlan({
68
+ templateId: draft.templateId,
69
+ variables: {},
70
+ projectRoot: input.projectRoot,
71
+ plan: generation,
72
+ });
73
+ return ok({ plan, generation, droppedOps, ready: true });
74
+ }
75
+ function coerceOperation(raw) {
76
+ const k = raw.kind;
77
+ switch (k) {
78
+ case 'create': {
79
+ const content = reqStr(raw, 'content');
80
+ if (content === null)
81
+ return fail('content');
82
+ const op = { kind: 'create', content };
83
+ addOptStr(op, raw, 'description');
84
+ return { ok: true, value: op };
85
+ }
86
+ case 'append': {
87
+ const snippet = reqStr(raw, 'snippet');
88
+ if (snippet === null)
89
+ return fail('snippet');
90
+ const op = { kind: 'append', snippet };
91
+ addOptStr(op, raw, 'ifMissing');
92
+ addOptStr(op, raw, 'description');
93
+ return { ok: true, value: op };
94
+ }
95
+ case 'insert-after':
96
+ case 'insert-before': {
97
+ const anchor = reqStr(raw, 'anchor');
98
+ const snippet = reqStr(raw, 'snippet');
99
+ if (anchor === null)
100
+ return fail('anchor');
101
+ if (snippet === null)
102
+ return fail('snippet');
103
+ const op = { kind: k, anchor, snippet };
104
+ addOptStr(op, raw, 'ifMissing');
105
+ addOptStr(op, raw, 'description');
106
+ return { ok: true, value: op };
107
+ }
108
+ case 'replace': {
109
+ const find = reqStr(raw, 'find');
110
+ const replaceWith = reqStrAllowEmpty(raw, 'replaceWith');
111
+ if (find === null)
112
+ return fail('find');
113
+ if (replaceWith === null)
114
+ return fail('replaceWith');
115
+ const op = { kind: 'replace', find, replaceWith };
116
+ const expectMatches = optNum(raw, 'expectMatches');
117
+ if (expectMatches !== undefined)
118
+ op.expectMatches = expectMatches;
119
+ addOptStr(op, raw, 'description');
120
+ return { ok: true, value: op };
121
+ }
122
+ case 'export': {
123
+ const from = reqStr(raw, 'from');
124
+ if (from === null)
125
+ return fail('from');
126
+ const op = { kind: 'export', from };
127
+ const symbols = optStrArr(raw, 'symbols');
128
+ if (symbols)
129
+ op.symbols = symbols;
130
+ addOptStr(op, raw, 'ifMissing');
131
+ addOptStr(op, raw, 'description');
132
+ return { ok: true, value: op };
133
+ }
134
+ case 'ensure-import': {
135
+ const from = reqStr(raw, 'from');
136
+ if (from === null)
137
+ return fail('from');
138
+ const op = { kind: 'ensure-import', from };
139
+ const symbols = optStrArr(raw, 'symbols');
140
+ if (symbols)
141
+ op.symbols = symbols;
142
+ const typeOnly = optBool(raw, 'typeOnly');
143
+ if (typeOnly !== undefined)
144
+ op.typeOnly = typeOnly;
145
+ addOptStr(op, raw, 'defaultBinding');
146
+ addOptStr(op, raw, 'namespaceBinding');
147
+ addOptStr(op, raw, 'description');
148
+ return { ok: true, value: op };
149
+ }
150
+ case 'insert-enum-entry': {
151
+ const enumName = reqStr(raw, 'enumName');
152
+ const entryName = reqStr(raw, 'entryName');
153
+ const entryValue = reqStr(raw, 'entryValue');
154
+ if (enumName === null)
155
+ return fail('enumName');
156
+ if (entryName === null)
157
+ return fail('entryName');
158
+ if (entryValue === null)
159
+ return fail('entryValue');
160
+ const op = { kind: 'insert-enum-entry', enumName, entryName, entryValue };
161
+ addOptStr(op, raw, 'description');
162
+ return { ok: true, value: op };
163
+ }
164
+ case 'insert-object-entry': {
165
+ const objectName = reqStr(raw, 'objectName');
166
+ const entryKey = reqStr(raw, 'entryKey');
167
+ const entryValue = reqStr(raw, 'entryValue');
168
+ if (objectName === null)
169
+ return fail('objectName');
170
+ if (entryKey === null)
171
+ return fail('entryKey');
172
+ if (entryValue === null)
173
+ return fail('entryValue');
174
+ const op = { kind: 'insert-object-entry', objectName, entryKey, entryValue };
175
+ const shorthand = optBool(raw, 'shorthand');
176
+ if (shorthand !== undefined)
177
+ op.shorthand = shorthand;
178
+ addOptStr(op, raw, 'description');
179
+ return { ok: true, value: op };
180
+ }
181
+ case 'insert-array-entry': {
182
+ const arrayName = reqStr(raw, 'arrayName');
183
+ const entryValue = reqStr(raw, 'entryValue');
184
+ if (arrayName === null)
185
+ return fail('arrayName');
186
+ if (entryValue === null)
187
+ return fail('entryValue');
188
+ const op = { kind: 'insert-array-entry', arrayName, entryValue };
189
+ addOptStr(op, raw, 'ifMissing');
190
+ addOptStr(op, raw, 'description');
191
+ return { ok: true, value: op };
192
+ }
193
+ case 'insert-before-closing-brace': {
194
+ const containerName = reqStr(raw, 'containerName');
195
+ const snippet = reqStr(raw, 'snippet');
196
+ if (containerName === null)
197
+ return fail('containerName');
198
+ if (snippet === null)
199
+ return fail('snippet');
200
+ const op = { kind: 'insert-before-closing-brace', containerName, snippet };
201
+ addOptStr(op, raw, 'ifMissing');
202
+ addOptStr(op, raw, 'description');
203
+ return { ok: true, value: op };
204
+ }
205
+ case 'insert-between-anchors': {
206
+ const beginAnchor = reqStr(raw, 'beginAnchor');
207
+ const endAnchor = reqStr(raw, 'endAnchor');
208
+ const snippet = reqStr(raw, 'snippet');
209
+ if (beginAnchor === null)
210
+ return fail('beginAnchor');
211
+ if (endAnchor === null)
212
+ return fail('endAnchor');
213
+ if (snippet === null)
214
+ return fail('snippet');
215
+ const op = { kind: 'insert-between-anchors', beginAnchor, endAnchor, snippet };
216
+ addOptStr(op, raw, 'ifMissing');
217
+ addOptStr(op, raw, 'description');
218
+ return { ok: true, value: op };
219
+ }
220
+ default:
221
+ return { ok: false, error: `unsupported op kind "${k}"` };
222
+ }
223
+ }
224
+ function fail(field) {
225
+ return { ok: false, error: `"${field}" must be a non-empty string` };
226
+ }
227
+ function reqStr(raw, field) {
228
+ const v = raw[field];
229
+ return typeof v === 'string' && v.length > 0 ? v : null;
230
+ }
231
+ function reqStrAllowEmpty(raw, field) {
232
+ const v = raw[field];
233
+ return typeof v === 'string' ? v : null;
234
+ }
235
+ function addOptStr(target, raw, field) {
236
+ const v = raw[field];
237
+ if (typeof v === 'string')
238
+ target[field] = v;
239
+ }
240
+ function optStrArr(raw, field) {
241
+ const v = raw[field];
242
+ if (Array.isArray(v) && v.every((s) => typeof s === 'string'))
243
+ return v;
244
+ return undefined;
245
+ }
246
+ function optBool(raw, field) {
247
+ const v = raw[field];
248
+ return typeof v === 'boolean' ? v : undefined;
249
+ }
250
+ function optNum(raw, field) {
251
+ const v = raw[field];
252
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
253
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"synthetic-plan.d.ts","sourceRoot":"","sources":["../src/synthetic-plan.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,eAAe,CAsCjB;AAuBD,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAsCzC"}
1
+ {"version":3,"file":"synthetic-plan.d.ts","sourceRoot":"","sources":["../src/synthetic-plan.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,eAAe,CAsCjB;AAwCD,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAWD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAoDzC"}
@@ -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 = '__';
@@ -62,12 +63,30 @@ export function evaluateSavedPlanInPlace(plan, projectRoot) {
62
63
  return out;
63
64
  }
64
65
  function applyOperation(projectRoot, relativePath, operation) {
65
- const absolutePath = nodePath.resolve(projectRoot, relativePath);
66
- const existing = existsSync(absolutePath) ? readFileSync(absolutePath, 'utf8') : null;
66
+ // Route through the single generator chokepoint instead of a bare resolve, so
67
+ // a traversal / absolute `relativePath` in a hand-crafted or tampered plan
68
+ // can't write OUTSIDE the project root. An unsafe path becomes a Conflict,
69
+ // which writeSyntheticPlan refuses (matching the template path in dry-run.ts).
70
+ let safe;
71
+ try {
72
+ safe = safeResolveTargetPath(relativePath, projectRoot);
73
+ }
74
+ catch (e) {
75
+ const pathErr = e;
76
+ return {
77
+ type: FileChangeType.Conflict,
78
+ absolutePath: pathErr.rawPath,
79
+ relativePath: pathErr.rawPath,
80
+ contents: '',
81
+ reason: `Refused unsafe target path (${pathErr.code}): ${pathErr.message}`,
82
+ sizeBytes: 0,
83
+ };
84
+ }
85
+ const existing = existsSync(safe.absolutePath) ? readFileSync(safe.absolutePath, 'utf8') : null;
67
86
  return evaluatePlannedChange({
68
- change: { targetPath: relativePath, operation },
69
- absolutePath,
70
- relativePath,
87
+ change: { targetPath: safe.relativePath, operation },
88
+ absolutePath: safe.absolutePath,
89
+ relativePath: safe.relativePath,
71
90
  existing,
72
91
  });
73
92
  }
@@ -78,10 +97,27 @@ function applyOperation(projectRoot, relativePath, operation) {
78
97
  */
79
98
  import { mkdirSync, writeFileSync } from 'node:fs';
80
99
  import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
100
+ /**
101
+ * A CCR retrieval marker (`<<ccr:<hex>…>>`) is a pointer into the compress
102
+ * cache — a LOSSY/compressed view, never apply-grade source. If one ever
103
+ * reaches a write (e.g. a compressed diff fed into a `create`/`replace` op), it
104
+ * would corrupt the file. This detector enforces, at the write chokepoint, the
105
+ * invariant that the compression layer only documents in a comment.
106
+ */
107
+ const CCR_MARKER_RE = /<<ccr:[0-9a-f]{8,}/;
81
108
  export function writeSyntheticPlan(plan) {
82
109
  if (plan.hasConflicts) {
83
110
  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
111
  }
112
+ // Refuse the WHOLE plan if any writeable change carries a CCR marker — a
113
+ // compressed/lossy blob must never be written as source.
114
+ for (const change of plan.changes) {
115
+ if (!isWriteableSyntheticChange(change.type))
116
+ continue;
117
+ if (CCR_MARKER_RE.test(change.contents)) {
118
+ 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 } }));
119
+ }
120
+ }
85
121
  const written = [];
86
122
  let totalBytes = 0;
87
123
  let skipped = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/generator",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
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.16",
47
- "@shrkcrft/templates": "^0.1.0-alpha.16",
48
- "@shrkcrft/rules": "^0.1.0-alpha.16",
49
- "@shrkcrft/paths": "^0.1.0-alpha.16"
46
+ "@shrkcrft/core": "^0.1.0-alpha.18",
47
+ "@shrkcrft/templates": "^0.1.0-alpha.18",
48
+ "@shrkcrft/rules": "^0.1.0-alpha.18",
49
+ "@shrkcrft/paths": "^0.1.0-alpha.18"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"