@shrkcrft/generator 0.1.0-alpha.1
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 +53 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML-frontmatter parser for spec.md.
|
|
3
|
+
*
|
|
4
|
+
* Supports the subset used by `sharkcraft.spec/v1` frontmatter:
|
|
5
|
+
* - `key: scalar` (string / number / boolean / null)
|
|
6
|
+
* - `key:` followed by a block-scalar `|` body
|
|
7
|
+
* - `key:` followed by ` - item` (array of scalars)
|
|
8
|
+
* - `key:` followed by ` - id: x` blocks (array of objects with scalar fields)
|
|
9
|
+
* - `key:` followed by ` subkey: value` (one-level nested objects)
|
|
10
|
+
*
|
|
11
|
+
* Quoted strings: `'...'` and `"..."` (no escape sequences beyond
|
|
12
|
+
* `\n`, `\\`, `\"`).
|
|
13
|
+
* Comments: `# ...` on their own line are stripped.
|
|
14
|
+
* Out-of-grammar input throws with a 1-based line number.
|
|
15
|
+
*
|
|
16
|
+
* Pure parser. No IO. Lives in `@shrkcrft/generator` so the spec
|
|
17
|
+
* model + parser can be reused from the CLI without pulling in the
|
|
18
|
+
* inspector.
|
|
19
|
+
*/
|
|
20
|
+
import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
|
|
21
|
+
const FRONTMATTER_DELIMITER = '---';
|
|
22
|
+
export function splitSpecMd(source) {
|
|
23
|
+
const lines = source.split('\n');
|
|
24
|
+
if (lines.length === 0 || lines[0].trim() !== FRONTMATTER_DELIMITER) {
|
|
25
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'spec.md must begin with `---` on its first line'));
|
|
26
|
+
}
|
|
27
|
+
let closeIdx = -1;
|
|
28
|
+
for (let i = 1; i < lines.length; i++) {
|
|
29
|
+
if (lines[i].trim() === FRONTMATTER_DELIMITER) {
|
|
30
|
+
closeIdx = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (closeIdx === -1) {
|
|
35
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'spec.md frontmatter not terminated (missing closing `---`)'));
|
|
36
|
+
}
|
|
37
|
+
const frontmatterLines = lines.slice(1, closeIdx);
|
|
38
|
+
const raw = frontmatterLines.join('\n');
|
|
39
|
+
const parsed = parseFrontmatter(raw);
|
|
40
|
+
if (!parsed.ok)
|
|
41
|
+
return err(parsed.error);
|
|
42
|
+
const body = lines.slice(closeIdx + 1).join('\n');
|
|
43
|
+
return ok({
|
|
44
|
+
frontmatter: { fields: parsed.value, raw },
|
|
45
|
+
body,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function parseFrontmatter(raw) {
|
|
49
|
+
const lines = raw.split('\n');
|
|
50
|
+
const fields = {};
|
|
51
|
+
let i = 0;
|
|
52
|
+
while (i < lines.length) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (line.length !== line.trimStart().length) {
|
|
60
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Top-level key must start at column 0 (line ${i + 1})`));
|
|
61
|
+
}
|
|
62
|
+
const colon = line.indexOf(':');
|
|
63
|
+
if (colon === -1) {
|
|
64
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Expected "<key>:" at line ${i + 1}`));
|
|
65
|
+
}
|
|
66
|
+
const key = line.slice(0, colon).trim();
|
|
67
|
+
if (!isValidKey(key)) {
|
|
68
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Invalid key "${key}" at line ${i + 1}`));
|
|
69
|
+
}
|
|
70
|
+
const remainder = line.slice(colon + 1);
|
|
71
|
+
const inline = remainder.trim();
|
|
72
|
+
if (inline === '|') {
|
|
73
|
+
const block = readBlockScalar(lines, i + 1);
|
|
74
|
+
if (!block.ok)
|
|
75
|
+
return err(block.error);
|
|
76
|
+
fields[key] = block.value.text;
|
|
77
|
+
i = block.value.nextIndex;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (inline.length > 0) {
|
|
81
|
+
const scalar = parseInlineScalar(inline, i + 1);
|
|
82
|
+
if (!scalar.ok)
|
|
83
|
+
return err(scalar.error);
|
|
84
|
+
fields[key] = scalar.value;
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// No inline value — look ahead for nested block.
|
|
89
|
+
const peek = peekNonBlank(lines, i + 1);
|
|
90
|
+
if (peek === null) {
|
|
91
|
+
fields[key] = null;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const indent = peek.line.length - peek.line.trimStart().length;
|
|
96
|
+
if (indent === 0) {
|
|
97
|
+
fields[key] = null;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const trimmedPeek = peek.line.trim();
|
|
102
|
+
if (trimmedPeek.startsWith('- ') || trimmedPeek === '-') {
|
|
103
|
+
const arr = parseArrayBlock(lines, i + 1, indent);
|
|
104
|
+
if (!arr.ok)
|
|
105
|
+
return err(arr.error);
|
|
106
|
+
fields[key] = arr.value.value;
|
|
107
|
+
i = arr.value.nextIndex;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Nested object block.
|
|
111
|
+
const obj = parseObjectBlock(lines, i + 1, indent);
|
|
112
|
+
if (!obj.ok)
|
|
113
|
+
return err(obj.error);
|
|
114
|
+
fields[key] = obj.value.value;
|
|
115
|
+
i = obj.value.nextIndex;
|
|
116
|
+
}
|
|
117
|
+
return ok(fields);
|
|
118
|
+
}
|
|
119
|
+
function isValidKey(key) {
|
|
120
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key);
|
|
121
|
+
}
|
|
122
|
+
function readBlockScalar(lines, start) {
|
|
123
|
+
let i = start;
|
|
124
|
+
let baseIndent = -1;
|
|
125
|
+
const collected = [];
|
|
126
|
+
while (i < lines.length) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
if (line.trim() === '') {
|
|
129
|
+
collected.push('');
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const indent = line.length - line.trimStart().length;
|
|
134
|
+
if (baseIndent === -1) {
|
|
135
|
+
if (indent === 0)
|
|
136
|
+
break;
|
|
137
|
+
baseIndent = indent;
|
|
138
|
+
}
|
|
139
|
+
if (indent < baseIndent)
|
|
140
|
+
break;
|
|
141
|
+
collected.push(line.slice(baseIndent));
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
while (collected.length > 0 && collected[collected.length - 1] === '') {
|
|
145
|
+
collected.pop();
|
|
146
|
+
}
|
|
147
|
+
return ok({ text: collected.join('\n'), nextIndex: i });
|
|
148
|
+
}
|
|
149
|
+
function peekNonBlank(lines, start) {
|
|
150
|
+
for (let i = start; i < lines.length; i++) {
|
|
151
|
+
const trimmed = lines[i].trim();
|
|
152
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
153
|
+
continue;
|
|
154
|
+
return { line: lines[i], index: i };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function parseArrayBlock(lines, start, expectedIndent) {
|
|
159
|
+
let i = start;
|
|
160
|
+
const scalars = [];
|
|
161
|
+
const objects = [];
|
|
162
|
+
let mode = 'unknown';
|
|
163
|
+
while (i < lines.length) {
|
|
164
|
+
const line = lines[i];
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
167
|
+
i++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const indent = line.length - line.trimStart().length;
|
|
171
|
+
if (indent < expectedIndent)
|
|
172
|
+
break;
|
|
173
|
+
if (indent !== expectedIndent) {
|
|
174
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Inconsistent indent at line ${i + 1} (expected ${expectedIndent}, got ${indent})`));
|
|
175
|
+
}
|
|
176
|
+
if (!trimmed.startsWith('-')) {
|
|
177
|
+
// Sibling at same indent — end of array.
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
const itemBody = trimmed.replace(/^-\s?/, '');
|
|
181
|
+
if (itemBody.length === 0) {
|
|
182
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Empty array item at line ${i + 1}`));
|
|
183
|
+
}
|
|
184
|
+
// Object item iff it contains an unquoted `:`.
|
|
185
|
+
const colonIdx = findUnquotedColon(itemBody);
|
|
186
|
+
if (colonIdx === -1) {
|
|
187
|
+
if (mode === 'object') {
|
|
188
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Mixed array kinds at line ${i + 1}`));
|
|
189
|
+
}
|
|
190
|
+
mode = 'scalar';
|
|
191
|
+
const scalar = parseInlineScalar(itemBody, i + 1);
|
|
192
|
+
if (!scalar.ok)
|
|
193
|
+
return err(scalar.error);
|
|
194
|
+
if (Array.isArray(scalar.value)) {
|
|
195
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Nested array values are not supported (line ${i + 1})`));
|
|
196
|
+
}
|
|
197
|
+
scalars.push(scalar.value);
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (mode === 'scalar') {
|
|
202
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Mixed array kinds at line ${i + 1}`));
|
|
203
|
+
}
|
|
204
|
+
mode = 'object';
|
|
205
|
+
const obj = {};
|
|
206
|
+
const firstKey = itemBody.slice(0, colonIdx).trim();
|
|
207
|
+
const firstVal = itemBody.slice(colonIdx + 1).trim();
|
|
208
|
+
if (!isValidKey(firstKey)) {
|
|
209
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Invalid object key "${firstKey}" at line ${i + 1}`));
|
|
210
|
+
}
|
|
211
|
+
if (firstVal.length > 0) {
|
|
212
|
+
const scalar = parseInlineScalar(firstVal, i + 1);
|
|
213
|
+
if (!scalar.ok)
|
|
214
|
+
return err(scalar.error);
|
|
215
|
+
obj[firstKey] = scalar.value;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
obj[firstKey] = null;
|
|
219
|
+
}
|
|
220
|
+
i++;
|
|
221
|
+
// Collect continuation lines: ` - id: x` was just consumed; further
|
|
222
|
+
// lines for THIS object must be indented at `expectedIndent + 2`.
|
|
223
|
+
const objIndent = expectedIndent + 2;
|
|
224
|
+
while (i < lines.length) {
|
|
225
|
+
const inner = lines[i];
|
|
226
|
+
const innerTrim = inner.trim();
|
|
227
|
+
if (innerTrim === '' || innerTrim.startsWith('#')) {
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const innerIndent = inner.length - inner.trimStart().length;
|
|
232
|
+
if (innerIndent < objIndent)
|
|
233
|
+
break;
|
|
234
|
+
if (innerIndent > objIndent) {
|
|
235
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Object continuation must be indented to column ${objIndent} (line ${i + 1})`));
|
|
236
|
+
}
|
|
237
|
+
const innerColon = findUnquotedColon(innerTrim);
|
|
238
|
+
if (innerColon === -1) {
|
|
239
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Expected "<key>: <value>" at line ${i + 1}`));
|
|
240
|
+
}
|
|
241
|
+
const k = innerTrim.slice(0, innerColon).trim();
|
|
242
|
+
const v = innerTrim.slice(innerColon + 1).trim();
|
|
243
|
+
if (!isValidKey(k)) {
|
|
244
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Invalid object key "${k}" at line ${i + 1}`));
|
|
245
|
+
}
|
|
246
|
+
if (v.length === 0) {
|
|
247
|
+
// Could be either a nested scalar array (e.g. `verifiedBy:`)
|
|
248
|
+
// OR a nested scalar object (e.g. `variables:`). Peek and dispatch.
|
|
249
|
+
const peek = peekNonBlank(lines, i + 1);
|
|
250
|
+
if (peek) {
|
|
251
|
+
const peekIndent = peek.line.length - peek.line.trimStart().length;
|
|
252
|
+
const peekTrim = peek.line.trim();
|
|
253
|
+
if (peekIndent > objIndent) {
|
|
254
|
+
if (peekTrim.startsWith('- ') || peekTrim === '-') {
|
|
255
|
+
const arr = parseArrayBlock(lines, i + 1, peekIndent);
|
|
256
|
+
if (!arr.ok)
|
|
257
|
+
return err(arr.error);
|
|
258
|
+
if (Array.isArray(arr.value.value) && arr.value.value.every(isScalarLike)) {
|
|
259
|
+
obj[k] = arr.value.value;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Nested object-arrays are not supported inside array-object items (line ${i + 1})`));
|
|
263
|
+
}
|
|
264
|
+
i = arr.value.nextIndex;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// Nested object block (e.g. variables: { name: foo }).
|
|
268
|
+
const sub = parseObjectBlock(lines, i + 1, peekIndent);
|
|
269
|
+
if (!sub.ok)
|
|
270
|
+
return err(sub.error);
|
|
271
|
+
const flat = {};
|
|
272
|
+
for (const [sk, sv] of Object.entries(sub.value.value)) {
|
|
273
|
+
if (!isScalarLike(sv)) {
|
|
274
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Nested objects within array-object items must contain scalar values only (line ${i + 1})`));
|
|
275
|
+
}
|
|
276
|
+
flat[sk] = sv;
|
|
277
|
+
}
|
|
278
|
+
obj[k] = flat;
|
|
279
|
+
i = sub.value.nextIndex;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
obj[k] = null;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const scalar = parseInlineScalar(v, i + 1);
|
|
287
|
+
if (!scalar.ok)
|
|
288
|
+
return err(scalar.error);
|
|
289
|
+
obj[k] = scalar.value;
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
objects.push(obj);
|
|
294
|
+
}
|
|
295
|
+
if (mode === 'object') {
|
|
296
|
+
return ok({ value: objects, nextIndex: i });
|
|
297
|
+
}
|
|
298
|
+
return ok({ value: scalars, nextIndex: i });
|
|
299
|
+
}
|
|
300
|
+
function parseObjectBlock(lines, start, expectedIndent) {
|
|
301
|
+
const obj = {};
|
|
302
|
+
let i = start;
|
|
303
|
+
while (i < lines.length) {
|
|
304
|
+
const line = lines[i];
|
|
305
|
+
const trimmed = line.trim();
|
|
306
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
307
|
+
i++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const indent = line.length - line.trimStart().length;
|
|
311
|
+
if (indent < expectedIndent)
|
|
312
|
+
break;
|
|
313
|
+
if (indent !== expectedIndent) {
|
|
314
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Inconsistent indent at line ${i + 1} (expected ${expectedIndent}, got ${indent})`));
|
|
315
|
+
}
|
|
316
|
+
const colon = findUnquotedColon(trimmed);
|
|
317
|
+
if (colon === -1) {
|
|
318
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Expected "<key>: <value>" at line ${i + 1}`));
|
|
319
|
+
}
|
|
320
|
+
const key = trimmed.slice(0, colon).trim();
|
|
321
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
322
|
+
if (!isValidKey(key)) {
|
|
323
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Invalid object key "${key}" at line ${i + 1}`));
|
|
324
|
+
}
|
|
325
|
+
if (value.length === 0) {
|
|
326
|
+
// Look ahead for an indented array (e.g. nested `packages:` with
|
|
327
|
+
// a ` - foo` sub-list).
|
|
328
|
+
const peek = peekNonBlank(lines, i + 1);
|
|
329
|
+
if (peek) {
|
|
330
|
+
const peekIndent = peek.line.length - peek.line.trimStart().length;
|
|
331
|
+
const peekTrim = peek.line.trim();
|
|
332
|
+
if (peekIndent > expectedIndent && (peekTrim.startsWith('- ') || peekTrim === '-')) {
|
|
333
|
+
const arr = parseArrayBlock(lines, i + 1, peekIndent);
|
|
334
|
+
if (!arr.ok)
|
|
335
|
+
return err(arr.error);
|
|
336
|
+
if (Array.isArray(arr.value.value) && arr.value.value.every(isScalarLike)) {
|
|
337
|
+
obj[key] = arr.value.value;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Nested object-arrays are not supported inside nested objects (line ${i + 1})`));
|
|
341
|
+
}
|
|
342
|
+
i = arr.value.nextIndex;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
obj[key] = null;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const scalar = parseInlineScalar(value, i + 1);
|
|
350
|
+
if (!scalar.ok)
|
|
351
|
+
return err(scalar.error);
|
|
352
|
+
obj[key] = scalar.value;
|
|
353
|
+
}
|
|
354
|
+
i++;
|
|
355
|
+
}
|
|
356
|
+
return ok({ value: obj, nextIndex: i });
|
|
357
|
+
}
|
|
358
|
+
function isScalarLike(v) {
|
|
359
|
+
return (v === null ||
|
|
360
|
+
typeof v === 'string' ||
|
|
361
|
+
typeof v === 'number' ||
|
|
362
|
+
typeof v === 'boolean');
|
|
363
|
+
}
|
|
364
|
+
function findUnquotedColon(s) {
|
|
365
|
+
let inSingle = false;
|
|
366
|
+
let inDouble = false;
|
|
367
|
+
for (let i = 0; i < s.length; i++) {
|
|
368
|
+
const c = s[i];
|
|
369
|
+
if (c === "'" && !inDouble)
|
|
370
|
+
inSingle = !inSingle;
|
|
371
|
+
else if (c === '"' && !inSingle)
|
|
372
|
+
inDouble = !inDouble;
|
|
373
|
+
else if (c === ':' && !inSingle && !inDouble)
|
|
374
|
+
return i;
|
|
375
|
+
}
|
|
376
|
+
return -1;
|
|
377
|
+
}
|
|
378
|
+
export function parseInlineScalar(s, line) {
|
|
379
|
+
const trimmed = stripTrailingComment(s).trim();
|
|
380
|
+
if (trimmed.length === 0)
|
|
381
|
+
return ok(null);
|
|
382
|
+
if (trimmed === 'null' || trimmed === '~')
|
|
383
|
+
return ok(null);
|
|
384
|
+
if (trimmed === 'true')
|
|
385
|
+
return ok(true);
|
|
386
|
+
if (trimmed === 'false')
|
|
387
|
+
return ok(false);
|
|
388
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
389
|
+
return ok(unescapeDoubleQuoted(trimmed.slice(1, -1)));
|
|
390
|
+
}
|
|
391
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
|
|
392
|
+
return ok(trimmed.slice(1, -1));
|
|
393
|
+
}
|
|
394
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
395
|
+
return parseInlineArray(trimmed, line);
|
|
396
|
+
}
|
|
397
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
398
|
+
return ok(Number.parseInt(trimmed, 10));
|
|
399
|
+
}
|
|
400
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) {
|
|
401
|
+
return ok(Number.parseFloat(trimmed));
|
|
402
|
+
}
|
|
403
|
+
// Bare string (no quotes). Forbid embedded control characters.
|
|
404
|
+
if (/[\x00-\x08\x0B-\x1F]/.test(trimmed)) {
|
|
405
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Control character in bare string at line ${line}`));
|
|
406
|
+
}
|
|
407
|
+
return ok(trimmed);
|
|
408
|
+
}
|
|
409
|
+
function parseInlineArray(s, line) {
|
|
410
|
+
const inner = s.slice(1, -1).trim();
|
|
411
|
+
if (inner.length === 0)
|
|
412
|
+
return ok([]);
|
|
413
|
+
const parts = splitTopLevelCommas(inner);
|
|
414
|
+
const out = [];
|
|
415
|
+
for (const part of parts) {
|
|
416
|
+
const trimmed = part.trim();
|
|
417
|
+
if (trimmed.length === 0)
|
|
418
|
+
continue;
|
|
419
|
+
const scalar = parseInlineScalar(trimmed, line);
|
|
420
|
+
if (!scalar.ok)
|
|
421
|
+
return err(scalar.error);
|
|
422
|
+
if (Array.isArray(scalar.value)) {
|
|
423
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Nested inline arrays are not supported (line ${line})`));
|
|
424
|
+
}
|
|
425
|
+
out.push(scalar.value);
|
|
426
|
+
}
|
|
427
|
+
return ok(out);
|
|
428
|
+
}
|
|
429
|
+
function splitTopLevelCommas(s) {
|
|
430
|
+
const out = [];
|
|
431
|
+
let cur = '';
|
|
432
|
+
let inSingle = false;
|
|
433
|
+
let inDouble = false;
|
|
434
|
+
let depth = 0;
|
|
435
|
+
for (let i = 0; i < s.length; i++) {
|
|
436
|
+
const c = s[i];
|
|
437
|
+
if (c === "'" && !inDouble)
|
|
438
|
+
inSingle = !inSingle;
|
|
439
|
+
else if (c === '"' && !inSingle)
|
|
440
|
+
inDouble = !inDouble;
|
|
441
|
+
else if (!inSingle && !inDouble) {
|
|
442
|
+
if (c === '[' || c === '{')
|
|
443
|
+
depth++;
|
|
444
|
+
else if (c === ']' || c === '}')
|
|
445
|
+
depth--;
|
|
446
|
+
else if (c === ',' && depth === 0) {
|
|
447
|
+
out.push(cur);
|
|
448
|
+
cur = '';
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
cur += c;
|
|
453
|
+
}
|
|
454
|
+
out.push(cur);
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
function stripTrailingComment(s) {
|
|
458
|
+
let inSingle = false;
|
|
459
|
+
let inDouble = false;
|
|
460
|
+
for (let i = 0; i < s.length; i++) {
|
|
461
|
+
const c = s[i];
|
|
462
|
+
if (c === "'" && !inDouble)
|
|
463
|
+
inSingle = !inSingle;
|
|
464
|
+
else if (c === '"' && !inSingle)
|
|
465
|
+
inDouble = !inDouble;
|
|
466
|
+
else if (c === '#' && !inSingle && !inDouble) {
|
|
467
|
+
const prev = s[i - 1];
|
|
468
|
+
if (prev === undefined || prev === ' ' || prev === '\t')
|
|
469
|
+
return s.slice(0, i);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return s;
|
|
473
|
+
}
|
|
474
|
+
function unescapeDoubleQuoted(s) {
|
|
475
|
+
let out = '';
|
|
476
|
+
for (let i = 0; i < s.length; i++) {
|
|
477
|
+
const c = s[i];
|
|
478
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
479
|
+
const next = s[i + 1];
|
|
480
|
+
if (next === 'n')
|
|
481
|
+
out += '\n';
|
|
482
|
+
else if (next === 't')
|
|
483
|
+
out += '\t';
|
|
484
|
+
else if (next === '\\')
|
|
485
|
+
out += '\\';
|
|
486
|
+
else if (next === '"')
|
|
487
|
+
out += '"';
|
|
488
|
+
else
|
|
489
|
+
out += next;
|
|
490
|
+
i++;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
out += c;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec id construction and conflict resolution.
|
|
3
|
+
*
|
|
4
|
+
* Spec ids are `<YYYY-MM-DD>-<slug>` where `<slug>` is kebab-case.
|
|
5
|
+
* Conflicts (same-day, same-slug) resolve via numeric suffixes `-2`,
|
|
6
|
+
* `-3`, etc. The id is the directory name under `.sharkcraft/specs/`,
|
|
7
|
+
* so it must be safe for any filesystem and stable across spec edits.
|
|
8
|
+
*/
|
|
9
|
+
export interface IBuildSpecIdInput {
|
|
10
|
+
/** Spec title or seed string. */
|
|
11
|
+
readonly title: string;
|
|
12
|
+
/** Optional explicit slug override (overrides title-derived slug). */
|
|
13
|
+
readonly slug?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Date string in YYYY-MM-DD form. Defaults to the current UTC date
|
|
16
|
+
* if omitted.
|
|
17
|
+
*/
|
|
18
|
+
readonly date?: string;
|
|
19
|
+
/** Pre-existing spec ids (full ids, not slugs) for collision detection. */
|
|
20
|
+
readonly existingIds?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface IBuiltSpecId {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly slug: string;
|
|
25
|
+
readonly date: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildSpecId(input: IBuildSpecIdInput): IBuiltSpecId;
|
|
28
|
+
export declare function normalizeSlug(input: string): string;
|
|
29
|
+
export declare function todayIsoDate(now?: Date): string;
|
|
30
|
+
//# sourceMappingURL=spec-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-id.d.ts","sourceRoot":"","sources":["../../src/spec/spec-id.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CASlE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED,wBAAgB,YAAY,CAAC,GAAG,GAAE,IAAiB,GAAG,MAAM,CAK3D"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec id construction and conflict resolution.
|
|
3
|
+
*
|
|
4
|
+
* Spec ids are `<YYYY-MM-DD>-<slug>` where `<slug>` is kebab-case.
|
|
5
|
+
* Conflicts (same-day, same-slug) resolve via numeric suffixes `-2`,
|
|
6
|
+
* `-3`, etc. The id is the directory name under `.sharkcraft/specs/`,
|
|
7
|
+
* so it must be safe for any filesystem and stable across spec edits.
|
|
8
|
+
*/
|
|
9
|
+
import { toKebabCase } from '@shrkcrft/core';
|
|
10
|
+
export function buildSpecId(input) {
|
|
11
|
+
const slug = normalizeSlug(input.slug ?? input.title);
|
|
12
|
+
const date = input.date ?? todayIsoDate();
|
|
13
|
+
const baseId = `${date}-${slug}`;
|
|
14
|
+
const existing = new Set(input.existingIds ?? []);
|
|
15
|
+
if (!existing.has(baseId))
|
|
16
|
+
return { id: baseId, slug, date };
|
|
17
|
+
let n = 2;
|
|
18
|
+
while (existing.has(`${baseId}-${n}`))
|
|
19
|
+
n++;
|
|
20
|
+
return { id: `${baseId}-${n}`, slug, date };
|
|
21
|
+
}
|
|
22
|
+
export function normalizeSlug(input) {
|
|
23
|
+
const k = toKebabCase(input)
|
|
24
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
25
|
+
.replace(/-+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '');
|
|
27
|
+
if (k.length === 0)
|
|
28
|
+
return 'spec';
|
|
29
|
+
if (!/^[a-z0-9]/.test(k))
|
|
30
|
+
return `s-${k}`;
|
|
31
|
+
return k;
|
|
32
|
+
}
|
|
33
|
+
export function todayIsoDate(now = new Date()) {
|
|
34
|
+
const y = now.getUTCFullYear().toString().padStart(4, '0');
|
|
35
|
+
const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
36
|
+
const d = now.getUTCDate().toString().padStart(2, '0');
|
|
37
|
+
return `${y}-${m}-${d}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write helpers for spec.md / spec.json / events.jsonl.
|
|
3
|
+
*
|
|
4
|
+
* Pure filesystem layer. Validation lives elsewhere; this module only
|
|
5
|
+
* reads/writes the on-disk artifacts. The directory layout is
|
|
6
|
+
* `.sharkcraft/specs/<id>/`:
|
|
7
|
+
*
|
|
8
|
+
* spec.md — frontmatter + body, authoritative
|
|
9
|
+
* spec.json — derived canonical view
|
|
10
|
+
* plan.json — signed combined plan (after `implement --write-plan`)
|
|
11
|
+
* verification.json — most recent verify report (after `verify`)
|
|
12
|
+
* events.jsonl — append-only log of `spec` operations
|
|
13
|
+
*/
|
|
14
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
15
|
+
import { SPEC_EVENTS_SCHEMA_V1, type ISpecJson } from './spec-model.js';
|
|
16
|
+
export declare const SPECS_DIR_RELATIVE = ".sharkcraft/specs";
|
|
17
|
+
export declare function specsRoot(projectRoot: string): string;
|
|
18
|
+
export declare function specDir(projectRoot: string, id: string): string;
|
|
19
|
+
export declare function specMdPath(projectRoot: string, id: string): string;
|
|
20
|
+
export declare function specJsonPath(projectRoot: string, id: string): string;
|
|
21
|
+
export declare function specPlanPath(projectRoot: string, id: string): string;
|
|
22
|
+
export declare function specVerificationPath(projectRoot: string, id: string): string;
|
|
23
|
+
export declare function specEventsPath(projectRoot: string, id: string): string;
|
|
24
|
+
export declare function listSpecIds(projectRoot: string): string[];
|
|
25
|
+
export declare function writeSpecMd(projectRoot: string, id: string, body: string): Result<void, AppError>;
|
|
26
|
+
export declare function readSpecMd(projectRoot: string, id: string): Result<string, AppError>;
|
|
27
|
+
export declare function writeSpecJson(projectRoot: string, id: string, json: ISpecJson): Result<void, AppError>;
|
|
28
|
+
export declare function readSpecJson(projectRoot: string, id: string): Result<ISpecJson, AppError>;
|
|
29
|
+
/**
|
|
30
|
+
* Convenience: parse spec.md, derive spec.json, return both. Does NOT
|
|
31
|
+
* read or write the cached spec.json on disk — caller decides.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadSpec(projectRoot: string, id: string): Result<{
|
|
34
|
+
spec: ISpecJson;
|
|
35
|
+
body: string;
|
|
36
|
+
}, AppError>;
|
|
37
|
+
export interface ISpecEvent {
|
|
38
|
+
readonly schema: typeof SPEC_EVENTS_SCHEMA_V1;
|
|
39
|
+
readonly ts: string;
|
|
40
|
+
readonly specId: string;
|
|
41
|
+
readonly operation: string;
|
|
42
|
+
readonly verdict?: string;
|
|
43
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
44
|
+
}
|
|
45
|
+
export declare function appendSpecEvent(projectRoot: string, id: string, event: Omit<ISpecEvent, 'schema' | 'ts' | 'specId'> & {
|
|
46
|
+
ts?: string;
|
|
47
|
+
}): Result<void, AppError>;
|
|
48
|
+
export declare function readSpecEvents(projectRoot: string, id: string): readonly ISpecEvent[];
|
|
49
|
+
export interface IPersistSpecArtifactsInput {
|
|
50
|
+
readonly projectRoot: string;
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly md: string;
|
|
53
|
+
readonly json: ISpecJson;
|
|
54
|
+
}
|
|
55
|
+
export declare function persistSpecArtifacts(input: IPersistSpecArtifactsInput): Result<void, AppError>;
|
|
56
|
+
//# sourceMappingURL=spec-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-io.d.ts","sourceRoot":"","sources":["../../src/spec/spec-io.ts"],"names":[],"mappings":"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"}
|