@shrkcrft/generator 0.1.0-alpha.30 → 0.1.0-alpha.31
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 +54 -11
- package/dist/generation-plan.d.ts +7 -0
- package/dist/generation-plan.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/planned-operation-fields.d.ts +71 -0
- package/dist/planned-operation-fields.d.ts.map +1 -0
- package/dist/planned-operation-fields.js +100 -0
- package/package.json +5 -5
package/dist/dry-run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../src/dry-run.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAY5D,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,CAqHf"}
|
package/dist/dry-run.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { safeResolveTargetPath } from '@shrkcrft/core';
|
|
3
|
-
import { renderTemplate, validateTemplateVariables, } from '@shrkcrft/templates';
|
|
3
|
+
import { renderTemplate, templateRemainderLines, validateTemplateVariables, } from '@shrkcrft/templates';
|
|
4
4
|
import { OverwriteStrategy } from "./overwrite-strategy.js";
|
|
5
5
|
import { FileChangeType } from "./file-change.js";
|
|
6
6
|
import { decideForExisting, summarizeConflicts } from "./conflict-handler.js";
|
|
7
7
|
import { buildNameVariables } from "./naming-strategy.js";
|
|
8
8
|
import { evaluatePlannedChange, } from "./planned-change.js";
|
|
9
|
+
import { describeInvalidPlannedOperation, validatePlannedOperation } from "./planned-operation-fields.js";
|
|
9
10
|
export function planGeneration(template, request) {
|
|
10
11
|
const warnings = [];
|
|
11
12
|
const overwriteStrategy = request.overwriteStrategy ?? OverwriteStrategy.Never;
|
|
@@ -24,6 +25,7 @@ export function planGeneration(template, request) {
|
|
|
24
25
|
hasConflicts: false,
|
|
25
26
|
warnings,
|
|
26
27
|
postGenerationNotes: template.postGenerationNotes ?? [],
|
|
28
|
+
remainderLines: templateRemainderLines(template),
|
|
27
29
|
},
|
|
28
30
|
safe: false,
|
|
29
31
|
};
|
|
@@ -90,10 +92,15 @@ export function planGeneration(template, request) {
|
|
|
90
92
|
}
|
|
91
93
|
// 2) v2 planned changes — evaluate against the live filesystem, threaded
|
|
92
94
|
// through the overlay so successive changes to one file compose.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
// Each op is shape-checked against PLANNED_OPERATION_FIELDS first, so a
|
|
96
|
+
// misspelled op becomes a located Conflict instead of a TypeError.
|
|
97
|
+
rendered.changes.forEach((tplChange, index) => {
|
|
98
|
+
const evaluated = planOne(tplChange, request.projectRoot, overlay, {
|
|
99
|
+
where: `template '${template.id}' change[${index}]`,
|
|
100
|
+
warnings,
|
|
101
|
+
});
|
|
95
102
|
changes.push(evaluated);
|
|
96
|
-
}
|
|
103
|
+
});
|
|
97
104
|
const { hasConflicts } = summarizeConflicts(changes);
|
|
98
105
|
return {
|
|
99
106
|
plan: {
|
|
@@ -104,12 +111,32 @@ export function planGeneration(template, request) {
|
|
|
104
111
|
hasConflicts,
|
|
105
112
|
warnings,
|
|
106
113
|
postGenerationNotes: rendered.postGenerationNotes,
|
|
114
|
+
remainderLines: templateRemainderLines(template),
|
|
107
115
|
},
|
|
108
116
|
safe: !hasConflicts && changes.length > 0,
|
|
109
117
|
};
|
|
110
118
|
}
|
|
111
|
-
function planOne(tplChange, projectRoot, overlay) {
|
|
119
|
+
function planOne(tplChange, projectRoot, overlay, ctx) {
|
|
112
120
|
const op = tplChange.operation;
|
|
121
|
+
const where = ctx?.where ?? 'change';
|
|
122
|
+
const shape = validatePlannedOperation(op);
|
|
123
|
+
const rawTarget = typeof tplChange.targetPath === 'string' ? tplChange.targetPath : '';
|
|
124
|
+
const invalid = describeInvalidPlannedOperation(shape, where) ??
|
|
125
|
+
(rawTarget.length === 0 ? `${where} (${shape.kind}): missing targetPath` : undefined);
|
|
126
|
+
if (invalid) {
|
|
127
|
+
return {
|
|
128
|
+
type: FileChangeType.Conflict,
|
|
129
|
+
absolutePath: rawTarget,
|
|
130
|
+
relativePath: rawTarget,
|
|
131
|
+
contents: '',
|
|
132
|
+
reason: invalid,
|
|
133
|
+
sizeBytes: 0,
|
|
134
|
+
operation: op,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (shape.unknown.length > 0) {
|
|
138
|
+
ctx?.warnings.push(`${where} (${shape.kind}): unknown key(s) ${shape.unknown.join(', ')} are ignored by the engine`);
|
|
139
|
+
}
|
|
113
140
|
let safe;
|
|
114
141
|
try {
|
|
115
142
|
safe = safeResolveTargetPath(tplChange.targetPath, projectRoot);
|
|
@@ -138,12 +165,28 @@ function planOne(tplChange, projectRoot, overlay) {
|
|
|
138
165
|
: existsSync(safe.absolutePath)
|
|
139
166
|
? readFileSafe(safe.absolutePath)
|
|
140
167
|
: null;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
let result;
|
|
169
|
+
try {
|
|
170
|
+
result = evaluatePlannedChange({
|
|
171
|
+
change,
|
|
172
|
+
absolutePath: safe.absolutePath,
|
|
173
|
+
relativePath: safe.relativePath,
|
|
174
|
+
existing,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
// A well-shaped op can still carry a wrong-typed value; the plan must name
|
|
179
|
+
// the change, never surface a bare TypeError.
|
|
180
|
+
return {
|
|
181
|
+
type: FileChangeType.Conflict,
|
|
182
|
+
absolutePath: safe.absolutePath,
|
|
183
|
+
relativePath: safe.relativePath,
|
|
184
|
+
contents: existing ?? '',
|
|
185
|
+
reason: `${where} (${shape.kind}): could not be evaluated — ${e.message}`,
|
|
186
|
+
sizeBytes: 0,
|
|
187
|
+
operation: op,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
147
190
|
// Record the cumulative content (Skip/Conflict carry the unchanged bytes,
|
|
148
191
|
// which is exactly what a later op on the same file should see).
|
|
149
192
|
overlay?.set(safe.absolutePath, result.contents);
|
|
@@ -8,6 +8,13 @@ export interface IGenerationPlan {
|
|
|
8
8
|
hasConflicts: boolean;
|
|
9
9
|
warnings: readonly string[];
|
|
10
10
|
postGenerationNotes: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* The template's declared remainder — what it deliberately does NOT
|
|
13
|
+
* scaffold, and the manual steps that cover it — as the printable lines
|
|
14
|
+
* `templateRemainderLines` renders (`@shrkcrft/templates`). Absent/empty
|
|
15
|
+
* when the template declares neither `notScaffolded` nor `manualSteps`.
|
|
16
|
+
*/
|
|
17
|
+
remainderLines?: readonly string[];
|
|
11
18
|
/**
|
|
12
19
|
* Optional folder operations attached to the plan. Carried verbatim
|
|
13
20
|
* into saved plans and executed via `applyFolderOps()` during `shrk apply`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generation-plan.d.ts","sourceRoot":"","sources":["../src/generation-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
|
1
|
+
{"version":3,"file":"generation-plan.d.ts","sourceRoot":"","sources":["../src/generation-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './naming-strategy.js';
|
|
|
9
9
|
export * from './saved-plan.js';
|
|
10
10
|
export * from './plan-signing.js';
|
|
11
11
|
export * from './planned-change.js';
|
|
12
|
+
export * from './planned-operation-fields.js';
|
|
12
13
|
export * from './folder-safety.js';
|
|
13
14
|
export * from './folder-apply.js';
|
|
14
15
|
export * from './synthetic-plan.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,4BAA4B,CAAC;AAC3C,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,+BAA+B,CAAC;AAC9C,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
|
@@ -9,6 +9,7 @@ export * from "./naming-strategy.js";
|
|
|
9
9
|
export * from "./saved-plan.js";
|
|
10
10
|
export * from "./plan-signing.js";
|
|
11
11
|
export * from "./planned-change.js";
|
|
12
|
+
export * from "./planned-operation-fields.js";
|
|
12
13
|
export * from "./folder-safety.js";
|
|
13
14
|
export * from "./folder-apply.js";
|
|
14
15
|
export * from "./synthetic-plan.js";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export declare const PLANNED_OPERATION_FIELDS: {
|
|
2
|
+
readonly create: {
|
|
3
|
+
readonly required: readonly ["content"];
|
|
4
|
+
readonly optional: readonly ["description"];
|
|
5
|
+
};
|
|
6
|
+
readonly append: {
|
|
7
|
+
readonly required: readonly ["snippet"];
|
|
8
|
+
readonly optional: readonly ["ifMissing", "description"];
|
|
9
|
+
};
|
|
10
|
+
readonly 'insert-after': {
|
|
11
|
+
readonly required: readonly ["anchor", "snippet"];
|
|
12
|
+
readonly optional: readonly ["ifMissing", "description"];
|
|
13
|
+
};
|
|
14
|
+
readonly 'insert-before': {
|
|
15
|
+
readonly required: readonly ["anchor", "snippet"];
|
|
16
|
+
readonly optional: readonly ["ifMissing", "description"];
|
|
17
|
+
};
|
|
18
|
+
readonly replace: {
|
|
19
|
+
readonly required: readonly ["find", "replaceWith"];
|
|
20
|
+
readonly optional: readonly ["expectMatches", "description"];
|
|
21
|
+
};
|
|
22
|
+
readonly export: {
|
|
23
|
+
readonly required: readonly ["from"];
|
|
24
|
+
readonly optional: readonly ["symbols", "ifMissing", "description"];
|
|
25
|
+
};
|
|
26
|
+
readonly 'ensure-import': {
|
|
27
|
+
readonly required: readonly ["from"];
|
|
28
|
+
readonly optional: readonly ["symbols", "typeOnly", "defaultBinding", "namespaceBinding", "description"];
|
|
29
|
+
};
|
|
30
|
+
readonly 'insert-enum-entry': {
|
|
31
|
+
readonly required: readonly ["enumName", "entryName", "entryValue"];
|
|
32
|
+
readonly optional: readonly ["description"];
|
|
33
|
+
};
|
|
34
|
+
readonly 'insert-object-entry': {
|
|
35
|
+
readonly required: readonly ["objectName", "entryKey", "entryValue"];
|
|
36
|
+
readonly optional: readonly ["shorthand", "description"];
|
|
37
|
+
};
|
|
38
|
+
readonly 'insert-array-entry': {
|
|
39
|
+
readonly required: readonly ["arrayName", "entryValue"];
|
|
40
|
+
readonly optional: readonly ["arrayNameAlternatives", "manualStepInstruction", "ifMissing", "description"];
|
|
41
|
+
};
|
|
42
|
+
readonly 'insert-before-closing-brace': {
|
|
43
|
+
readonly required: readonly ["containerName", "snippet"];
|
|
44
|
+
readonly optional: readonly ["ifMissing", "description"];
|
|
45
|
+
};
|
|
46
|
+
readonly 'insert-between-anchors': {
|
|
47
|
+
readonly required: readonly ["beginAnchor", "endAnchor", "snippet"];
|
|
48
|
+
readonly optional: readonly ["ifMissing", "description"];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Check one rendered op against the table. `kindKnown: false` means the kind is
|
|
53
|
+
* not a planned-operation kind at all; `missing` lists required fields that are
|
|
54
|
+
* absent; `unknown` lists keys the evaluator would silently drop; `suggestions`
|
|
55
|
+
* pairs an unknown key with the missing field it most likely meant
|
|
56
|
+
* (`value→entryValue`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function validatePlannedOperation(op: unknown): {
|
|
59
|
+
readonly kindKnown: boolean;
|
|
60
|
+
readonly kind: string;
|
|
61
|
+
readonly missing: readonly string[];
|
|
62
|
+
readonly unknown: readonly string[];
|
|
63
|
+
readonly suggestions: readonly string[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* One sentence naming what is wrong with an op, or `undefined` when the op is
|
|
67
|
+
* well-formed (unknown extra keys alone are reported by the caller as a
|
|
68
|
+
* warning, not here).
|
|
69
|
+
*/
|
|
70
|
+
export declare function describeInvalidPlannedOperation(shape: ReturnType<typeof validatePlannedOperation>, where: string): string | undefined;
|
|
71
|
+
//# sourceMappingURL=planned-operation-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planned-operation-fields.d.ts","sourceRoot":"","sources":["../src/planned-operation-fields.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BpC,CAAC;AA2BF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,OAAO,GAAG;IACrD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,CAuBA;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,EAClD,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CASpB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const PLANNED_OPERATION_FIELDS = {
|
|
2
|
+
create: { required: ['content'], optional: ['description'] },
|
|
3
|
+
append: { required: ['snippet'], optional: ['ifMissing', 'description'] },
|
|
4
|
+
'insert-after': { required: ['anchor', 'snippet'], optional: ['ifMissing', 'description'] },
|
|
5
|
+
'insert-before': { required: ['anchor', 'snippet'], optional: ['ifMissing', 'description'] },
|
|
6
|
+
replace: { required: ['find', 'replaceWith'], optional: ['expectMatches', 'description'] },
|
|
7
|
+
export: { required: ['from'], optional: ['symbols', 'ifMissing', 'description'] },
|
|
8
|
+
'ensure-import': {
|
|
9
|
+
required: ['from'],
|
|
10
|
+
optional: ['symbols', 'typeOnly', 'defaultBinding', 'namespaceBinding', 'description'],
|
|
11
|
+
},
|
|
12
|
+
'insert-enum-entry': { required: ['enumName', 'entryName', 'entryValue'], optional: ['description'] },
|
|
13
|
+
'insert-object-entry': {
|
|
14
|
+
required: ['objectName', 'entryKey', 'entryValue'],
|
|
15
|
+
optional: ['shorthand', 'description'],
|
|
16
|
+
},
|
|
17
|
+
'insert-array-entry': {
|
|
18
|
+
required: ['arrayName', 'entryValue'],
|
|
19
|
+
optional: ['arrayNameAlternatives', 'manualStepInstruction', 'ifMissing', 'description'],
|
|
20
|
+
},
|
|
21
|
+
'insert-before-closing-brace': {
|
|
22
|
+
required: ['containerName', 'snippet'],
|
|
23
|
+
optional: ['ifMissing', 'description'],
|
|
24
|
+
},
|
|
25
|
+
'insert-between-anchors': {
|
|
26
|
+
required: ['beginAnchor', 'endAnchor', 'snippet'],
|
|
27
|
+
optional: ['ifMissing', 'description'],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
/** Explicit near-misses authors write for a required field. */
|
|
31
|
+
const NEAR_MISS = {
|
|
32
|
+
key: ['entryKey'],
|
|
33
|
+
value: ['entryValue', 'snippet', 'content'],
|
|
34
|
+
name: ['entryName', 'arrayName', 'objectName', 'enumName', 'containerName'],
|
|
35
|
+
body: ['content', 'snippet'],
|
|
36
|
+
text: ['snippet', 'content'],
|
|
37
|
+
code: ['snippet', 'content'],
|
|
38
|
+
snippet: ['content'],
|
|
39
|
+
content: ['snippet'],
|
|
40
|
+
replace: ['replaceWith'],
|
|
41
|
+
with: ['replaceWith'],
|
|
42
|
+
search: ['find'],
|
|
43
|
+
pattern: ['find'],
|
|
44
|
+
begin: ['beginAnchor'],
|
|
45
|
+
end: ['endAnchor'],
|
|
46
|
+
target: ['arrayName', 'objectName', 'enumName', 'containerName'],
|
|
47
|
+
};
|
|
48
|
+
function isPresent(value) {
|
|
49
|
+
if (value === undefined || value === null)
|
|
50
|
+
return false;
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return true;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check one rendered op against the table. `kindKnown: false` means the kind is
|
|
57
|
+
* not a planned-operation kind at all; `missing` lists required fields that are
|
|
58
|
+
* absent; `unknown` lists keys the evaluator would silently drop; `suggestions`
|
|
59
|
+
* pairs an unknown key with the missing field it most likely meant
|
|
60
|
+
* (`value→entryValue`).
|
|
61
|
+
*/
|
|
62
|
+
export function validatePlannedOperation(op) {
|
|
63
|
+
if (!op || typeof op !== 'object') {
|
|
64
|
+
return { kindKnown: false, kind: String(op), missing: [], unknown: [], suggestions: [] };
|
|
65
|
+
}
|
|
66
|
+
const record = op;
|
|
67
|
+
const kind = typeof record.kind === 'string' ? record.kind : String(record.kind);
|
|
68
|
+
const spec = PLANNED_OPERATION_FIELDS[kind];
|
|
69
|
+
if (!spec)
|
|
70
|
+
return { kindKnown: false, kind, missing: [], unknown: [], suggestions: [] };
|
|
71
|
+
const missing = spec.required.filter((f) => !isPresent(record[f]));
|
|
72
|
+
const allowed = new Set(['kind', ...spec.required, ...spec.optional]);
|
|
73
|
+
const unknown = Object.keys(record).filter((k) => !allowed.has(k));
|
|
74
|
+
const suggestions = [];
|
|
75
|
+
for (const u of unknown) {
|
|
76
|
+
const explicit = (NEAR_MISS[u.toLowerCase()] ?? []).filter((f) => missing.includes(f));
|
|
77
|
+
const contained = missing.filter((f) => !explicit.includes(f) && f.toLowerCase().includes(u.toLowerCase()) && u.length >= 3);
|
|
78
|
+
const target = explicit[0] ?? contained[0];
|
|
79
|
+
if (target)
|
|
80
|
+
suggestions.push(`${u}→${target}`);
|
|
81
|
+
}
|
|
82
|
+
return { kindKnown: true, kind, missing, unknown, suggestions };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One sentence naming what is wrong with an op, or `undefined` when the op is
|
|
86
|
+
* well-formed (unknown extra keys alone are reported by the caller as a
|
|
87
|
+
* warning, not here).
|
|
88
|
+
*/
|
|
89
|
+
export function describeInvalidPlannedOperation(shape, where) {
|
|
90
|
+
if (!shape.kindKnown) {
|
|
91
|
+
return `${where}: unknown operation kind "${shape.kind}" — must be one of ${Object.keys(PLANNED_OPERATION_FIELDS).join(', ')}`;
|
|
92
|
+
}
|
|
93
|
+
if (shape.missing.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const parts = [`missing required ${shape.missing.join(', ')}`];
|
|
96
|
+
if (shape.unknown.length > 0)
|
|
97
|
+
parts.push(`unknown keys ${shape.unknown.join(', ')}`);
|
|
98
|
+
const hint = shape.suggestions.length > 0 ? ` — did you mean ${shape.suggestions.join(', ')}?` : '';
|
|
99
|
+
return `${where} (${shape.kind}): ${parts.join('; ')}${hint}`;
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/generator",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.31",
|
|
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.
|
|
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.31",
|
|
47
|
+
"@shrkcrft/templates": "^0.1.0-alpha.31",
|
|
48
|
+
"@shrkcrft/rules": "^0.1.0-alpha.31",
|
|
49
|
+
"@shrkcrft/paths": "^0.1.0-alpha.31"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|