@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/conflict-handler.d.ts +12 -0
  4. package/dist/conflict-handler.d.ts.map +1 -0
  5. package/dist/conflict-handler.js +25 -0
  6. package/dist/dry-run.d.ts +10 -0
  7. package/dist/dry-run.d.ts.map +1 -0
  8. package/dist/dry-run.js +178 -0
  9. package/dist/file-change.d.ts +46 -0
  10. package/dist/file-change.d.ts.map +1 -0
  11. package/dist/file-change.js +22 -0
  12. package/dist/folder-apply.d.ts +29 -0
  13. package/dist/folder-apply.d.ts.map +1 -0
  14. package/dist/folder-apply.js +117 -0
  15. package/dist/folder-safety.d.ts +12 -0
  16. package/dist/folder-safety.d.ts.map +1 -0
  17. package/dist/folder-safety.js +75 -0
  18. package/dist/generation-plan.d.ts +24 -0
  19. package/dist/generation-plan.d.ts.map +1 -0
  20. package/dist/generation-plan.js +1 -0
  21. package/dist/generation-request.d.ts +14 -0
  22. package/dist/generation-request.d.ts.map +1 -0
  23. package/dist/generation-request.js +1 -0
  24. package/dist/generator-engine.d.ts +12 -0
  25. package/dist/generator-engine.d.ts.map +1 -0
  26. package/dist/generator-engine.js +74 -0
  27. package/dist/grounding/extracted-plan.d.ts +42 -0
  28. package/dist/grounding/extracted-plan.d.ts.map +1 -0
  29. package/dist/grounding/extracted-plan.js +12 -0
  30. package/dist/grounding/extractor-registry.d.ts +21 -0
  31. package/dist/grounding/extractor-registry.d.ts.map +1 -0
  32. package/dist/grounding/extractor-registry.js +30 -0
  33. package/dist/grounding/extractor.d.ts +24 -0
  34. package/dist/grounding/extractor.d.ts.map +1 -0
  35. package/dist/grounding/extractor.js +8 -0
  36. package/dist/grounding/extractors/markdown-frontmatter-loose.d.ts +17 -0
  37. package/dist/grounding/extractors/markdown-frontmatter-loose.d.ts.map +1 -0
  38. package/dist/grounding/extractors/markdown-frontmatter-loose.js +160 -0
  39. package/dist/grounding/extractors/sharkcraft-spec-v1.d.ts +12 -0
  40. package/dist/grounding/extractors/sharkcraft-spec-v1.d.ts.map +1 -0
  41. package/dist/grounding/extractors/sharkcraft-spec-v1.js +56 -0
  42. package/dist/grounding/index.d.ts +6 -0
  43. package/dist/grounding/index.d.ts.map +1 -0
  44. package/dist/grounding/index.js +5 -0
  45. package/dist/index.d.ts +17 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +16 -0
  48. package/dist/naming-strategy.d.ts +5 -0
  49. package/dist/naming-strategy.d.ts.map +1 -0
  50. package/dist/naming-strategy.js +28 -0
  51. package/dist/overwrite-strategy.d.ts +14 -0
  52. package/dist/overwrite-strategy.d.ts.map +1 -0
  53. package/dist/overwrite-strategy.js +15 -0
  54. package/dist/plan-signing.d.ts +37 -0
  55. package/dist/plan-signing.d.ts.map +1 -0
  56. package/dist/plan-signing.js +82 -0
  57. package/dist/planned-change.d.ts +167 -0
  58. package/dist/planned-change.d.ts.map +1 -0
  59. package/dist/planned-change.js +507 -0
  60. package/dist/saved-plan.d.ts +110 -0
  61. package/dist/saved-plan.d.ts.map +1 -0
  62. package/dist/saved-plan.js +281 -0
  63. package/dist/spec/index.d.ts +7 -0
  64. package/dist/spec/index.d.ts.map +1 -0
  65. package/dist/spec/index.js +6 -0
  66. package/dist/spec/spec-derive.d.ts +15 -0
  67. package/dist/spec/spec-derive.d.ts.map +1 -0
  68. package/dist/spec/spec-derive.js +294 -0
  69. package/dist/spec/spec-frontmatter.d.ts +37 -0
  70. package/dist/spec/spec-frontmatter.d.ts.map +1 -0
  71. package/dist/spec/spec-frontmatter.js +497 -0
  72. package/dist/spec/spec-id.d.ts +30 -0
  73. package/dist/spec/spec-id.d.ts.map +1 -0
  74. package/dist/spec/spec-id.js +38 -0
  75. package/dist/spec/spec-io.d.ts +56 -0
  76. package/dist/spec/spec-io.d.ts.map +1 -0
  77. package/dist/spec/spec-io.js +176 -0
  78. package/dist/spec/spec-model.d.ts +117 -0
  79. package/dist/spec/spec-model.d.ts.map +1 -0
  80. package/dist/spec/spec-model.js +225 -0
  81. package/dist/spec/spec-scaffold.d.ts +32 -0
  82. package/dist/spec/spec-scaffold.d.ts.map +1 -0
  83. package/dist/spec/spec-scaffold.js +106 -0
  84. package/dist/synthetic-plan.d.ts +14 -0
  85. package/dist/synthetic-plan.d.ts.map +1 -0
  86. package/dist/synthetic-plan.js +123 -0
  87. package/package.json +53 -0
@@ -0,0 +1,281 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
5
+ /** v1 schema marker — kept for legacy CREATE-only plans. */
6
+ export const SAVED_PLAN_SCHEMA_V1 = 'sharkcraft.plan/v1';
7
+ /** v2 schema marker — emitted when any change carries a v2 operation. */
8
+ export const SAVED_PLAN_SCHEMA_V2 = 'sharkcraft.plan/v2';
9
+ /** Default exported alias — points at v1 for backward-compat with consumers. */
10
+ export const SAVED_PLAN_SCHEMA = SAVED_PLAN_SCHEMA_V1;
11
+ /**
12
+ * Build a saved plan. If any change in the plan carries a v2 operation, the
13
+ * resulting plan is tagged `sharkcraft.plan/v2`; otherwise v1.
14
+ */
15
+ export function buildSavedPlan(input) {
16
+ const hasV2Op = input.plan.changes.some((c) => c.operation !== undefined);
17
+ const hasFolderOps = (input.folderOps?.length ?? 0) > 0;
18
+ const expectedChanges = input.plan.changes.map((c) => {
19
+ const entry = {
20
+ type: String(c.type),
21
+ relativePath: c.relativePath,
22
+ sizeBytes: c.sizeBytes,
23
+ };
24
+ if (c.operation !== undefined)
25
+ entry.operation = c.operation;
26
+ return entry;
27
+ });
28
+ const out = {
29
+ schema: hasV2Op || hasFolderOps ? SAVED_PLAN_SCHEMA_V2 : SAVED_PLAN_SCHEMA_V1,
30
+ templateId: input.templateId,
31
+ variables: { ...input.variables },
32
+ projectRoot: input.projectRoot,
33
+ createdAt: new Date().toISOString(),
34
+ expectedChanges,
35
+ };
36
+ if (input.name !== undefined)
37
+ out.name = input.name;
38
+ if (input.note !== undefined)
39
+ out.note = input.note;
40
+ if (hasFolderOps)
41
+ out.folderOps = [...input.folderOps];
42
+ return out;
43
+ }
44
+ export function savePlanToFile(plan, filePath) {
45
+ try {
46
+ mkdirSync(dirname(filePath), { recursive: true });
47
+ writeFileSync(filePath, JSON.stringify(plan, null, 2) + '\n', 'utf8');
48
+ return ok(undefined);
49
+ }
50
+ catch (e) {
51
+ return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, `Failed to save plan: ${filePath}`, {
52
+ details: { filePath },
53
+ cause: e,
54
+ }));
55
+ }
56
+ }
57
+ export function readPlanFromFile(filePath) {
58
+ if (!existsSync(filePath)) {
59
+ return err(new AppErrorImpl(ERROR_CODES.NOT_FOUND, `Plan file not found: ${filePath}`, {
60
+ details: { filePath },
61
+ }));
62
+ }
63
+ let raw;
64
+ try {
65
+ raw = readFileSync(filePath, 'utf8');
66
+ }
67
+ catch (e) {
68
+ return err(new AppErrorImpl(ERROR_CODES.FILE_READ_ERROR, `Failed to read plan: ${filePath}`, {
69
+ details: { filePath },
70
+ cause: e,
71
+ }));
72
+ }
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(raw);
76
+ }
77
+ catch (e) {
78
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Plan file is not valid JSON: ${filePath}`, {
79
+ cause: e,
80
+ }));
81
+ }
82
+ const validation = validateSavedPlanShape(parsed);
83
+ if (!validation.ok)
84
+ return err(validation.error);
85
+ return ok(validation.value);
86
+ }
87
+ function validateSavedPlanShape(value) {
88
+ if (!value || typeof value !== 'object') {
89
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan must be a JSON object'));
90
+ }
91
+ const obj = value;
92
+ if (obj.schema !== SAVED_PLAN_SCHEMA_V1 && obj.schema !== SAVED_PLAN_SCHEMA_V2) {
93
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Unsupported plan schema: ${String(obj.schema)} (expected ${SAVED_PLAN_SCHEMA_V1} or ${SAVED_PLAN_SCHEMA_V2})`));
94
+ }
95
+ if (typeof obj.templateId !== 'string' || !obj.templateId) {
96
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: templateId must be a non-empty string'));
97
+ }
98
+ if (obj.variables === null || typeof obj.variables !== 'object') {
99
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: variables must be an object'));
100
+ }
101
+ for (const [k, v] of Object.entries(obj.variables)) {
102
+ if (typeof v !== 'string') {
103
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Plan: variables.${k} must be a string (got ${typeof v})`));
104
+ }
105
+ }
106
+ if (typeof obj.projectRoot !== 'string' || !obj.projectRoot) {
107
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: projectRoot must be a non-empty string'));
108
+ }
109
+ if (typeof obj.createdAt !== 'string') {
110
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: createdAt must be a string'));
111
+ }
112
+ if (obj.name !== undefined && typeof obj.name !== 'string') {
113
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: name must be a string'));
114
+ }
115
+ if (obj.folderOps !== undefined) {
116
+ if (!Array.isArray(obj.folderOps)) {
117
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: folderOps must be an array'));
118
+ }
119
+ for (const f of obj.folderOps) {
120
+ if (!f || typeof f !== 'object') {
121
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: folderOps entries must be objects'));
122
+ }
123
+ const fo = f;
124
+ if (fo.kind !== 'rename-folder' && fo.kind !== 'delete-folder') {
125
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Plan: folderOps.kind must be "rename-folder" or "delete-folder" (got ${String(fo.kind)})`));
126
+ }
127
+ if (typeof fo.targetPath !== 'string' || fo.targetPath.length === 0) {
128
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: folderOps.targetPath must be a non-empty string'));
129
+ }
130
+ if (fo.newPath !== undefined && typeof fo.newPath !== 'string') {
131
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: folderOps.newPath must be a string'));
132
+ }
133
+ if (fo.kind === 'rename-folder' && (fo.newPath === undefined || fo.newPath.length === 0)) {
134
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'Plan: folderOps[rename-folder] requires newPath'));
135
+ }
136
+ }
137
+ }
138
+ return ok(obj);
139
+ }
140
+ /**
141
+ * Compare the saved plan's expected changes with a freshly-computed plan's
142
+ * changes. Returns an empty array when they match. v2 plans additionally
143
+ * detect operation-intent drift (e.g. signed `append` became `replace` in
144
+ * the template).
145
+ */
146
+ export function diffPlanChanges(saved, fresh) {
147
+ if (!saved.expectedChanges)
148
+ return [];
149
+ // Key by `path :: operation-fingerprint` so multiple ops on the
150
+ // same file are tracked independently. Falls back to path-only keying
151
+ // for v1 plans where `operation` is absent. Reordered independent ops
152
+ // still match (the fingerprint is stable and does not depend on order).
153
+ const out = [];
154
+ const expectedByKey = new Map();
155
+ for (const e of saved.expectedChanges) {
156
+ expectedByKey.set(`${e.relativePath}::${opFingerprint(e.operation, e.type)}`, e);
157
+ }
158
+ const freshByKey = new Map();
159
+ for (const c of fresh.changes) {
160
+ freshByKey.set(`${c.relativePath}::${opFingerprint(c.operation, c.type)}`, c);
161
+ }
162
+ for (const [key, expected] of expectedByKey) {
163
+ const actual = freshByKey.get(key);
164
+ if (!actual) {
165
+ // Try a same-path fallback to give a more useful "type-changed" /
166
+ // "operation-changed" diagnostic when paths match but fingerprint
167
+ // differs.
168
+ const samePath = [...freshByKey.values()].find((c) => c.relativePath === expected.relativePath);
169
+ if (samePath) {
170
+ if (String(samePath.type) !== expected.type) {
171
+ out.push({
172
+ relativePath: expected.relativePath,
173
+ kind: 'type-changed',
174
+ detail: `${expected.type} → ${String(samePath.type)}`,
175
+ });
176
+ }
177
+ else if (expected.operation !== undefined &&
178
+ samePath.operation !== undefined &&
179
+ !operationsEqual(expected.operation, samePath.operation)) {
180
+ out.push({
181
+ relativePath: expected.relativePath,
182
+ kind: 'operation-changed',
183
+ detail: `${expected.operation.kind} intent drifted`,
184
+ });
185
+ }
186
+ else {
187
+ out.push({
188
+ relativePath: expected.relativePath,
189
+ kind: 'removed',
190
+ detail: `expected op fingerprint not found on this path`,
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+ out.push({ relativePath: expected.relativePath, kind: 'removed' });
196
+ continue;
197
+ }
198
+ if (actual.sizeBytes !== expected.sizeBytes) {
199
+ out.push({
200
+ relativePath: expected.relativePath,
201
+ kind: 'size-changed',
202
+ detail: `${expected.sizeBytes}B → ${actual.sizeBytes}B`,
203
+ });
204
+ }
205
+ }
206
+ for (const [key, actual] of freshByKey) {
207
+ if (!expectedByKey.has(key)) {
208
+ // Only surface as `added` if the same-path expected didn't already
209
+ // produce a type-changed / operation-changed diagnostic above.
210
+ const samePathExpected = saved.expectedChanges.some((e) => e.relativePath === actual.relativePath);
211
+ if (!samePathExpected) {
212
+ out.push({ relativePath: actual.relativePath, kind: 'added' });
213
+ }
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+ /**
219
+ * Stable, canonical fingerprint for a plan operation. Two semantically
220
+ * equal operations on the same file produce the same fingerprint; reordering
221
+ * independent ops keeps each one's fingerprint stable.
222
+ */
223
+ function opFingerprint(op, fallbackType) {
224
+ if (!op)
225
+ return `legacy:${fallbackType}`;
226
+ // The canonical-JSON encoding sorts keys and ignores undefined values, so
227
+ // the resulting hash is deterministic.
228
+ return canonicalOpString(op);
229
+ }
230
+ function canonicalOpString(op) {
231
+ return JSON.stringify(canon(op));
232
+ }
233
+ /**
234
+ * Structural equality on operations. JSON round-trip; cheap and deterministic.
235
+ */
236
+ function operationsEqual(a, b) {
237
+ return JSON.stringify(canon(a)) === JSON.stringify(canon(b));
238
+ }
239
+ /**
240
+ * Folder-op divergence. Returns an empty array when saved and live
241
+ * folder ops match (by kind+targetPath+newPath).
242
+ */
243
+ export function diffPlanFolderOps(saved, liveFolderOps) {
244
+ const out = [];
245
+ const savedOps = saved.folderOps ?? [];
246
+ const key = (op) => `${op.kind}:${op.targetPath}:${op.newPath ?? ''}`;
247
+ const savedByKey = new Map();
248
+ for (const o of savedOps)
249
+ savedByKey.set(key(o), o);
250
+ const liveByKey = new Map();
251
+ for (const o of liveFolderOps)
252
+ liveByKey.set(key(o), o);
253
+ for (const [k, savedOp] of savedByKey) {
254
+ if (!liveByKey.has(k)) {
255
+ out.push({
256
+ relativePath: savedOp.targetPath,
257
+ kind: 'removed',
258
+ detail: `folder-op ${savedOp.kind}`,
259
+ });
260
+ }
261
+ }
262
+ for (const [k, liveOp] of liveByKey) {
263
+ if (!savedByKey.has(k)) {
264
+ out.push({
265
+ relativePath: liveOp.targetPath,
266
+ kind: 'added',
267
+ detail: `folder-op ${liveOp.kind}`,
268
+ });
269
+ }
270
+ }
271
+ return out;
272
+ }
273
+ function canon(op) {
274
+ const out = {};
275
+ for (const k of Object.keys(op).sort()) {
276
+ const v = op[k];
277
+ if (v !== undefined)
278
+ out[k] = v;
279
+ }
280
+ return out;
281
+ }
@@ -0,0 +1,7 @@
1
+ export * from './spec-frontmatter.js';
2
+ export * from './spec-model.js';
3
+ export * from './spec-id.js';
4
+ export * from './spec-derive.js';
5
+ export * from './spec-scaffold.js';
6
+ export * from './spec-io.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spec/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from "./spec-frontmatter.js";
2
+ export * from "./spec-model.js";
3
+ export * from "./spec-id.js";
4
+ export * from "./spec-derive.js";
5
+ export * from "./spec-scaffold.js";
6
+ export * from "./spec-io.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Derive the canonical `spec.json` view from a parsed `spec.md`.
3
+ *
4
+ * Pure transformation: parsed frontmatter + body → `ISpecJson`. The
5
+ * frontmatterHash / bodyHash are sha256 hex digests. Frontmatter
6
+ * canonicalization sorts keys recursively so the hash is stable
7
+ * across YAML formatting changes that preserve semantics.
8
+ */
9
+ import { type AppError, type Result } from '@shrkcrft/core';
10
+ import type { IParsedSpecMd } from './spec-frontmatter.js';
11
+ import { type ISpecJson } from './spec-model.js';
12
+ export declare function deriveSpecJson(parsed: IParsedSpecMd): Result<ISpecJson, AppError>;
13
+ export declare function sha256(text: string): string;
14
+ export declare function canonicalJson(value: unknown): string;
15
+ //# sourceMappingURL=spec-derive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-derive.d.ts","sourceRoot":"","sources":["../../src/spec/spec-derive.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAoB,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAQL,KAAK,SAAS,EAKf,MAAM,iBAAiB,CAAC;AAEzB,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAwEjF;AA8LD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcpD"}
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Derive the canonical `spec.json` view from a parsed `spec.md`.
3
+ *
4
+ * Pure transformation: parsed frontmatter + body → `ISpecJson`. The
5
+ * frontmatterHash / bodyHash are sha256 hex digests. Frontmatter
6
+ * canonicalization sorts keys recursively so the hash is stable
7
+ * across YAML formatting changes that preserve semantics.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
11
+ import { knownTopLevelKeys, SPEC_SCHEMA_V1, SpecStatus, } from "./spec-model.js";
12
+ export function deriveSpecJson(parsed) {
13
+ const fm = parsed.frontmatter.fields;
14
+ const knownKeys = new Set(knownTopLevelKeys());
15
+ const unknownKeys = [];
16
+ for (const k of Object.keys(fm)) {
17
+ if (!knownKeys.has(k))
18
+ unknownKeys.push(k);
19
+ }
20
+ const id = asString(fm['id']);
21
+ const slug = asString(fm['slug']);
22
+ const title = asString(fm['title']);
23
+ const status = asSpecStatus(fm['status']);
24
+ const createdAt = asString(fm['createdAt']);
25
+ const updatedAt = asString(fm['updatedAt']);
26
+ const intent = asString(fm['intent']);
27
+ const motivation = asString(fm['motivation']);
28
+ if (!id) {
29
+ return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'spec.md frontmatter missing required "id"'));
30
+ }
31
+ const acceptanceCriteria = readAcceptanceCriteria(fm['acceptanceCriteria']);
32
+ const affectedAreas = readAffectedAreas(fm['affectedAreas']);
33
+ const relevantRules = readScalarArray(fm['relevantRules']);
34
+ const relevantKnowledge = readScalarArray(fm['relevantKnowledge']);
35
+ const relevantPaths = readScalarArray(fm['relevantPaths']);
36
+ const proposedTemplates = readProposedTemplates(fm['proposedTemplates']);
37
+ const risks = readRisks(fm['risks']);
38
+ const outOfScope = readScalarArray(fm['outOfScope']);
39
+ const externalLinks = readExternalLinks(fm['externalLinks']);
40
+ const boundariesCheck = readBoundariesCheck(fm['boundariesCheck']);
41
+ const verificationCommands = readVerificationCommandRefs(fm['verificationCommands']);
42
+ const plan = readPlanRef(fm['plan']);
43
+ const bodyHash = sha256(parsed.body);
44
+ // The frontmatter hash includes EVERYTHING in the frontmatter EXCEPT
45
+ // the `plan` block (which is added after implement). That way the
46
+ // hash is stable from create → review → implement.
47
+ const hashableFm = {};
48
+ for (const [k, v] of Object.entries(fm)) {
49
+ if (k === 'plan')
50
+ continue;
51
+ hashableFm[k] = v;
52
+ }
53
+ const frontmatterHash = sha256(canonicalJson(hashableFm));
54
+ const json = {
55
+ schema: SPEC_SCHEMA_V1,
56
+ id,
57
+ slug: slug || '',
58
+ title: title || '',
59
+ status,
60
+ createdAt: createdAt || '',
61
+ updatedAt: updatedAt || '',
62
+ intent: intent || '',
63
+ motivation: motivation || '',
64
+ acceptanceCriteria,
65
+ affectedAreas,
66
+ relevantRules,
67
+ relevantKnowledge,
68
+ relevantPaths,
69
+ proposedTemplates,
70
+ risks,
71
+ outOfScope,
72
+ externalLinks,
73
+ boundariesCheck: { predicted: boundariesCheck },
74
+ verificationCommands,
75
+ ...(plan ? { plan } : {}),
76
+ frontmatterHash,
77
+ bodyHash,
78
+ unknownKeys,
79
+ };
80
+ return ok(json);
81
+ }
82
+ function asString(v) {
83
+ if (typeof v === 'string')
84
+ return v;
85
+ if (typeof v === 'number' || typeof v === 'boolean')
86
+ return String(v);
87
+ return '';
88
+ }
89
+ function asSpecStatus(v) {
90
+ const s = typeof v === 'string' ? v : '';
91
+ if (Object.values(SpecStatus).includes(s))
92
+ return s;
93
+ return SpecStatus.Draft;
94
+ }
95
+ function readAcceptanceCriteria(v) {
96
+ if (!Array.isArray(v))
97
+ return [];
98
+ const out = [];
99
+ for (const item of v) {
100
+ if (item === null || typeof item !== 'object')
101
+ continue;
102
+ const obj = item;
103
+ const id = typeof obj['id'] === 'string' ? obj['id'] : '';
104
+ const text = typeof obj['text'] === 'string' ? obj['text'] : '';
105
+ const verifiedByRaw = obj['verifiedBy'];
106
+ const verifiedBy = readVerifiedBy(verifiedByRaw);
107
+ out.push({ id, text, verifiedBy });
108
+ }
109
+ return out;
110
+ }
111
+ function readVerifiedBy(raw) {
112
+ if (typeof raw === 'string') {
113
+ // YAML inline arrays aren't supported by our parser; allow a comma-list as a courtesy.
114
+ return raw
115
+ .split(',')
116
+ .map((s) => s.trim())
117
+ .filter((s) => s.length > 0);
118
+ }
119
+ if (Array.isArray(raw)) {
120
+ return raw
121
+ .map((v) => (typeof v === 'string' ? v : String(v ?? '')))
122
+ .filter((s) => s.length > 0);
123
+ }
124
+ return [];
125
+ }
126
+ function readAffectedAreas(v) {
127
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
128
+ return { files: [], packages: [], layers: [] };
129
+ }
130
+ const obj = v;
131
+ return {
132
+ files: readScalarArrayLike(obj['files']),
133
+ packages: readScalarArrayLike(obj['packages']),
134
+ layers: readScalarArrayLike(obj['layers']),
135
+ };
136
+ }
137
+ function readScalarArrayLike(v) {
138
+ if (typeof v === 'string') {
139
+ return v
140
+ .split(',')
141
+ .map((s) => s.trim())
142
+ .filter((s) => s.length > 0);
143
+ }
144
+ if (Array.isArray(v)) {
145
+ return v
146
+ .map((x) => (typeof x === 'string' ? x : String(x ?? '')))
147
+ .filter((s) => s.length > 0);
148
+ }
149
+ return [];
150
+ }
151
+ function readScalarArray(v) {
152
+ if (Array.isArray(v)) {
153
+ return v
154
+ .map((x) => (typeof x === 'string' ? x : String(x ?? '')))
155
+ .filter((s) => s.length > 0);
156
+ }
157
+ if (typeof v === 'string') {
158
+ return v
159
+ .split(',')
160
+ .map((s) => s.trim())
161
+ .filter((s) => s.length > 0);
162
+ }
163
+ return [];
164
+ }
165
+ function readProposedTemplates(v) {
166
+ if (!Array.isArray(v))
167
+ return [];
168
+ const out = [];
169
+ for (const item of v) {
170
+ if (item === null || typeof item !== 'object')
171
+ continue;
172
+ const obj = item;
173
+ const templateId = typeof obj['templateId'] === 'string' ? obj['templateId'] : '';
174
+ if (!templateId)
175
+ continue;
176
+ const note = typeof obj['note'] === 'string' ? obj['note'] : undefined;
177
+ const variables = {};
178
+ const rawVars = obj['variables'];
179
+ if (rawVars && typeof rawVars === 'object' && !Array.isArray(rawVars)) {
180
+ for (const [k, val] of Object.entries(rawVars)) {
181
+ variables[k] = typeof val === 'string' ? val : String(val ?? '');
182
+ }
183
+ }
184
+ out.push(note !== undefined ? { templateId, variables, note } : { templateId, variables });
185
+ }
186
+ return out;
187
+ }
188
+ function readRisks(v) {
189
+ if (!Array.isArray(v))
190
+ return [];
191
+ const out = [];
192
+ for (const item of v) {
193
+ if (item === null || typeof item !== 'object')
194
+ continue;
195
+ const obj = item;
196
+ const id = typeof obj['id'] === 'string' ? obj['id'] : '';
197
+ const text = typeof obj['text'] === 'string' ? obj['text'] : '';
198
+ if (!id || !text)
199
+ continue;
200
+ const mitigation = typeof obj['mitigation'] === 'string' ? obj['mitigation'] : undefined;
201
+ out.push(mitigation !== undefined ? { id, text, mitigation } : { id, text });
202
+ }
203
+ return out;
204
+ }
205
+ function readExternalLinks(v) {
206
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
207
+ return { issue: null, pr: null };
208
+ }
209
+ const obj = v;
210
+ return {
211
+ issue: scalarOrNull(obj['issue']),
212
+ pr: scalarOrNull(obj['pr']),
213
+ };
214
+ }
215
+ function scalarOrNull(v) {
216
+ if (typeof v === 'string')
217
+ return v.length > 0 ? v : null;
218
+ if (v === null || v === undefined)
219
+ return null;
220
+ return String(v);
221
+ }
222
+ function readBoundariesCheck(v) {
223
+ // Support either `boundariesCheck: { predicted: [...] }` (nested) or
224
+ // `boundariesCheck:` shorthand with predicted as the direct array.
225
+ if (!v || typeof v !== 'object' || Array.isArray(v))
226
+ return [];
227
+ const obj = v;
228
+ const raw = obj['predicted'];
229
+ if (!Array.isArray(raw))
230
+ return [];
231
+ const out = [];
232
+ for (const item of raw) {
233
+ if (item === null || typeof item !== 'object')
234
+ continue;
235
+ const o = item;
236
+ const from = typeof o['from'] === 'string' ? o['from'] : '';
237
+ const to = typeof o['to'] === 'string' ? o['to'] : '';
238
+ const reason = typeof o['reason'] === 'string' ? o['reason'] : '';
239
+ if (!from || !to)
240
+ continue;
241
+ out.push({ from, to, reason });
242
+ }
243
+ return out;
244
+ }
245
+ function readVerificationCommandRefs(v) {
246
+ if (Array.isArray(v)) {
247
+ const out = [];
248
+ for (const item of v) {
249
+ if (typeof item === 'string') {
250
+ if (item.length > 0)
251
+ out.push({ id: item });
252
+ }
253
+ else if (item && typeof item === 'object') {
254
+ const id = item['id'];
255
+ if (typeof id === 'string' && id.length > 0)
256
+ out.push({ id });
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+ return [];
262
+ }
263
+ function readPlanRef(v) {
264
+ if (!v || typeof v !== 'object' || Array.isArray(v))
265
+ return undefined;
266
+ const obj = v;
267
+ const planPath = typeof obj['planPath'] === 'string' ? obj['planPath'] : '';
268
+ const planHash = typeof obj['planHash'] === 'string' ? obj['planHash'] : '';
269
+ if (!planPath || !planHash)
270
+ return undefined;
271
+ const signedAt = typeof obj['signedAt'] === 'string' ? obj['signedAt'] : undefined;
272
+ return signedAt !== undefined ? { planPath, planHash, signedAt } : { planPath, planHash };
273
+ }
274
+ export function sha256(text) {
275
+ return createHash('sha256').update(text, 'utf8').digest('hex');
276
+ }
277
+ export function canonicalJson(value) {
278
+ if (value === null || value === undefined)
279
+ return JSON.stringify(value ?? null);
280
+ if (typeof value !== 'object')
281
+ return JSON.stringify(value);
282
+ if (Array.isArray(value)) {
283
+ return '[' + value.map((v) => canonicalJson(v)).join(',') + ']';
284
+ }
285
+ const keys = Object.keys(value).sort();
286
+ const parts = [];
287
+ for (const k of keys) {
288
+ const v = value[k];
289
+ if (v === undefined)
290
+ continue;
291
+ parts.push(JSON.stringify(k) + ':' + canonicalJson(v));
292
+ }
293
+ return '{' + parts.join(',') + '}';
294
+ }
@@ -0,0 +1,37 @@
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 { type AppError, type Result } from '@shrkcrft/core';
21
+ export type FrontmatterScalar = string | number | boolean | null;
22
+ export type FrontmatterFieldValue = FrontmatterScalar | readonly FrontmatterScalar[] | Readonly<Record<string, FrontmatterScalar>>;
23
+ export type FrontmatterValue = FrontmatterScalar | readonly FrontmatterScalar[] | ReadonlyArray<Readonly<Record<string, FrontmatterFieldValue>>> | Readonly<Record<string, FrontmatterFieldValue>>;
24
+ export interface IFrontmatterDocument {
25
+ readonly fields: Readonly<Record<string, FrontmatterValue>>;
26
+ /** Original frontmatter text (between the `---` delimiters), for hashing. */
27
+ readonly raw: string;
28
+ }
29
+ export interface IParsedSpecMd {
30
+ readonly frontmatter: IFrontmatterDocument;
31
+ /** Markdown body (everything after the closing `---`). */
32
+ readonly body: string;
33
+ }
34
+ export declare function splitSpecMd(source: string): Result<IParsedSpecMd, AppError>;
35
+ export declare function parseFrontmatter(raw: string): Result<Readonly<Record<string, FrontmatterValue>>, AppError>;
36
+ export declare function parseInlineScalar(s: string, line: number): Result<FrontmatterScalar | readonly FrontmatterScalar[], AppError>;
37
+ //# sourceMappingURL=spec-frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-frontmatter.d.ts","sourceRoot":"","sources":["../../src/spec/spec-frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEhG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AACjE,MAAM,MAAM,qBAAqB,GAC7B,iBAAiB,GACjB,SAAS,iBAAiB,EAAE,GAC5B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAChD,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,SAAS,iBAAiB,EAAE,GAC5B,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC9D,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC5D,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAID,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,CAkC3E;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GACV,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAoF9D;AAiWD,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,iBAAiB,GAAG,SAAS,iBAAiB,EAAE,EAAE,QAAQ,CAAC,CA+BpE"}