@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,176 @@
|
|
|
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 { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from 'node:fs';
|
|
15
|
+
import * as nodePath from 'node:path';
|
|
16
|
+
import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
|
|
17
|
+
import { deriveSpecJson } from "./spec-derive.js";
|
|
18
|
+
import { splitSpecMd } from "./spec-frontmatter.js";
|
|
19
|
+
import { SPEC_EVENTS_SCHEMA_V1 } from "./spec-model.js";
|
|
20
|
+
export const SPECS_DIR_RELATIVE = '.sharkcraft/specs';
|
|
21
|
+
export function specsRoot(projectRoot) {
|
|
22
|
+
return nodePath.join(projectRoot, SPECS_DIR_RELATIVE);
|
|
23
|
+
}
|
|
24
|
+
export function specDir(projectRoot, id) {
|
|
25
|
+
return nodePath.join(specsRoot(projectRoot), id);
|
|
26
|
+
}
|
|
27
|
+
export function specMdPath(projectRoot, id) {
|
|
28
|
+
return nodePath.join(specDir(projectRoot, id), 'spec.md');
|
|
29
|
+
}
|
|
30
|
+
export function specJsonPath(projectRoot, id) {
|
|
31
|
+
return nodePath.join(specDir(projectRoot, id), 'spec.json');
|
|
32
|
+
}
|
|
33
|
+
export function specPlanPath(projectRoot, id) {
|
|
34
|
+
return nodePath.join(specDir(projectRoot, id), 'plan.json');
|
|
35
|
+
}
|
|
36
|
+
export function specVerificationPath(projectRoot, id) {
|
|
37
|
+
return nodePath.join(specDir(projectRoot, id), 'verification.json');
|
|
38
|
+
}
|
|
39
|
+
export function specEventsPath(projectRoot, id) {
|
|
40
|
+
return nodePath.join(specDir(projectRoot, id), 'events.jsonl');
|
|
41
|
+
}
|
|
42
|
+
export function listSpecIds(projectRoot) {
|
|
43
|
+
const root = specsRoot(projectRoot);
|
|
44
|
+
if (!existsSync(root))
|
|
45
|
+
return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const entry of readdirSync(root)) {
|
|
48
|
+
const full = nodePath.join(root, entry);
|
|
49
|
+
let s = null;
|
|
50
|
+
try {
|
|
51
|
+
s = statSync(full);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (s?.isDirectory()) {
|
|
57
|
+
const md = nodePath.join(full, 'spec.md');
|
|
58
|
+
if (existsSync(md))
|
|
59
|
+
out.push(entry);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
out.sort().reverse();
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function writeSpecMd(projectRoot, id, body) {
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(specDir(projectRoot, id), { recursive: true });
|
|
68
|
+
writeFileSync(specMdPath(projectRoot, id), body.endsWith('\n') ? body : body + '\n', 'utf8');
|
|
69
|
+
return ok(undefined);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, 'Failed to write spec.md', { cause: e }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function readSpecMd(projectRoot, id) {
|
|
76
|
+
const p = specMdPath(projectRoot, id);
|
|
77
|
+
if (!existsSync(p)) {
|
|
78
|
+
return err(new AppErrorImpl(ERROR_CODES.NOT_FOUND, `Spec not found: ${id} (expected ${p})`));
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return ok(readFileSync(p, 'utf8'));
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_READ_ERROR, `Failed to read spec.md for ${id}`, { cause: e }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function writeSpecJson(projectRoot, id, json) {
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(specDir(projectRoot, id), { recursive: true });
|
|
90
|
+
writeFileSync(specJsonPath(projectRoot, id), JSON.stringify(json, null, 2) + '\n', 'utf8');
|
|
91
|
+
return ok(undefined);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, 'Failed to write spec.json', { cause: e }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function readSpecJson(projectRoot, id) {
|
|
98
|
+
const p = specJsonPath(projectRoot, id);
|
|
99
|
+
if (!existsSync(p)) {
|
|
100
|
+
return err(new AppErrorImpl(ERROR_CODES.NOT_FOUND, `spec.json not found for ${id}`));
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync(p, 'utf8');
|
|
104
|
+
return ok(JSON.parse(raw));
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, `Invalid spec.json for ${id}`, { cause: e }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Convenience: parse spec.md, derive spec.json, return both. Does NOT
|
|
112
|
+
* read or write the cached spec.json on disk — caller decides.
|
|
113
|
+
*/
|
|
114
|
+
export function loadSpec(projectRoot, id) {
|
|
115
|
+
const raw = readSpecMd(projectRoot, id);
|
|
116
|
+
if (!raw.ok)
|
|
117
|
+
return err(raw.error);
|
|
118
|
+
const parsed = splitSpecMd(raw.value);
|
|
119
|
+
if (!parsed.ok)
|
|
120
|
+
return err(parsed.error);
|
|
121
|
+
const derived = deriveSpecJson(parsed.value);
|
|
122
|
+
if (!derived.ok)
|
|
123
|
+
return err(derived.error);
|
|
124
|
+
return ok({ spec: derived.value, body: parsed.value.body });
|
|
125
|
+
}
|
|
126
|
+
export function appendSpecEvent(projectRoot, id, event) {
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(specDir(projectRoot, id), { recursive: true });
|
|
129
|
+
const entry = {
|
|
130
|
+
schema: SPEC_EVENTS_SCHEMA_V1,
|
|
131
|
+
ts: event.ts ?? new Date().toISOString(),
|
|
132
|
+
specId: id,
|
|
133
|
+
operation: event.operation,
|
|
134
|
+
...(event.verdict !== undefined ? { verdict: event.verdict } : {}),
|
|
135
|
+
...(event.details !== undefined ? { details: event.details } : {}),
|
|
136
|
+
};
|
|
137
|
+
appendFileSync(specEventsPath(projectRoot, id), JSON.stringify(entry) + '\n', 'utf8');
|
|
138
|
+
return ok(undefined);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return err(new AppErrorImpl(ERROR_CODES.FILE_WRITE_ERROR, 'Failed to append spec event', { cause: e }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function readSpecEvents(projectRoot, id) {
|
|
145
|
+
const p = specEventsPath(projectRoot, id);
|
|
146
|
+
if (!existsSync(p))
|
|
147
|
+
return [];
|
|
148
|
+
try {
|
|
149
|
+
const raw = readFileSync(p, 'utf8');
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const line of raw.split('\n')) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (trimmed.length === 0)
|
|
154
|
+
continue;
|
|
155
|
+
try {
|
|
156
|
+
out.push(JSON.parse(trimmed));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Skip malformed lines.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function persistSpecArtifacts(input) {
|
|
169
|
+
const mdRes = writeSpecMd(input.projectRoot, input.id, input.md);
|
|
170
|
+
if (!mdRes.ok)
|
|
171
|
+
return err(mdRes.error);
|
|
172
|
+
const jsonRes = writeSpecJson(input.projectRoot, input.id, input.json);
|
|
173
|
+
if (!jsonRes.ok)
|
|
174
|
+
return err(jsonRes.error);
|
|
175
|
+
return ok(undefined);
|
|
176
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sharkcraft.spec/v1` model + structural validation.
|
|
3
|
+
*
|
|
4
|
+
* The spec frontmatter is the authoritative source of truth; the markdown
|
|
5
|
+
* body is inert documentation. `spec.json` is the canonical machine-
|
|
6
|
+
* readable view, derived deterministically (see `spec-derive.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Structural validation lives here so the generator package can validate
|
|
9
|
+
* a spec without pulling in the inspector. Cross-registry validation
|
|
10
|
+
* (rule / knowledge / template / verification command resolution) lives
|
|
11
|
+
* in `@shrkcrft/inspector`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SPEC_SCHEMA_V1 = "sharkcraft.spec/v1";
|
|
14
|
+
export declare const SPEC_EVENTS_SCHEMA_V1 = "sharkcraft.spec-events/v1";
|
|
15
|
+
export declare enum SpecStatus {
|
|
16
|
+
Draft = "draft",
|
|
17
|
+
Review = "review",
|
|
18
|
+
Implementing = "implementing",
|
|
19
|
+
Implemented = "implemented",
|
|
20
|
+
Verified = "verified",
|
|
21
|
+
Abandoned = "abandoned"
|
|
22
|
+
}
|
|
23
|
+
export declare const SPEC_STATUS_VALUES: readonly SpecStatus[];
|
|
24
|
+
export interface ISpecAcceptanceCriterion {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly verifiedBy: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export interface ISpecAffectedAreas {
|
|
30
|
+
readonly files: readonly string[];
|
|
31
|
+
readonly packages: readonly string[];
|
|
32
|
+
readonly layers: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export interface ISpecProposedTemplate {
|
|
35
|
+
readonly templateId: string;
|
|
36
|
+
readonly variables: Readonly<Record<string, string>>;
|
|
37
|
+
readonly note?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ISpecRisk {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly mitigation?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ISpecExternalLinks {
|
|
45
|
+
readonly issue?: string | null;
|
|
46
|
+
readonly pr?: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface ISpecBoundaryPrediction {
|
|
49
|
+
readonly from: string;
|
|
50
|
+
readonly to: string;
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ISpecVerificationCommandRef {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ISpecPlanRef {
|
|
57
|
+
readonly planPath: string;
|
|
58
|
+
readonly planHash: string;
|
|
59
|
+
readonly signedAt?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The canonical machine-readable view of a spec. Derived from `spec.md`
|
|
63
|
+
* frontmatter. The `bodyHash` / `frontmatterHash` are sha256 hex digests.
|
|
64
|
+
*/
|
|
65
|
+
export interface ISpecJson {
|
|
66
|
+
readonly schema: typeof SPEC_SCHEMA_V1;
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly slug: string;
|
|
69
|
+
readonly title: string;
|
|
70
|
+
readonly status: SpecStatus;
|
|
71
|
+
readonly createdAt: string;
|
|
72
|
+
readonly updatedAt: string;
|
|
73
|
+
readonly intent: string;
|
|
74
|
+
readonly motivation: string;
|
|
75
|
+
readonly acceptanceCriteria: readonly ISpecAcceptanceCriterion[];
|
|
76
|
+
readonly affectedAreas: ISpecAffectedAreas;
|
|
77
|
+
readonly relevantRules: readonly string[];
|
|
78
|
+
readonly relevantKnowledge: readonly string[];
|
|
79
|
+
readonly relevantPaths: readonly string[];
|
|
80
|
+
readonly proposedTemplates: readonly ISpecProposedTemplate[];
|
|
81
|
+
readonly risks: readonly ISpecRisk[];
|
|
82
|
+
readonly outOfScope: readonly string[];
|
|
83
|
+
readonly externalLinks: ISpecExternalLinks;
|
|
84
|
+
readonly boundariesCheck: {
|
|
85
|
+
readonly predicted: readonly ISpecBoundaryPrediction[];
|
|
86
|
+
};
|
|
87
|
+
readonly verificationCommands: readonly ISpecVerificationCommandRef[];
|
|
88
|
+
readonly plan?: ISpecPlanRef;
|
|
89
|
+
readonly frontmatterHash: string;
|
|
90
|
+
readonly bodyHash: string;
|
|
91
|
+
/** Unknown frontmatter keys preserved so review reports can flag them. */
|
|
92
|
+
readonly unknownKeys: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
export interface ISpecValidationIssue {
|
|
95
|
+
readonly code: string;
|
|
96
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
97
|
+
readonly field: string;
|
|
98
|
+
readonly message: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The structural validation pass — does NOT touch external registries.
|
|
102
|
+
* Returns `errors` for blocking problems, `warnings` for advisory ones.
|
|
103
|
+
* Cross-registry checks (do the referenced rule/knowledge/template ids
|
|
104
|
+
* resolve?) live in `@shrkcrft/inspector`.
|
|
105
|
+
*/
|
|
106
|
+
export interface ISpecStructuralValidation {
|
|
107
|
+
readonly errors: readonly ISpecValidationIssue[];
|
|
108
|
+
readonly warnings: readonly ISpecValidationIssue[];
|
|
109
|
+
}
|
|
110
|
+
export declare function knownTopLevelKeys(): readonly string[];
|
|
111
|
+
/** Default spec body length cap, in bytes. */
|
|
112
|
+
export declare const DEFAULT_SPEC_BODY_MAX_BYTES = 16384;
|
|
113
|
+
export declare function validateSpecStructural(spec: ISpecJson, body: string, options?: {
|
|
114
|
+
bodyMaxBytes?: number;
|
|
115
|
+
}): ISpecStructuralValidation;
|
|
116
|
+
export declare function isSpecIdShape(id: string): boolean;
|
|
117
|
+
//# sourceMappingURL=spec-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-model.d.ts","sourceRoot":"","sources":["../../src/spec/spec-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sharkcraft.spec/v1` model + structural validation.
|
|
3
|
+
*
|
|
4
|
+
* The spec frontmatter is the authoritative source of truth; the markdown
|
|
5
|
+
* body is inert documentation. `spec.json` is the canonical machine-
|
|
6
|
+
* readable view, derived deterministically (see `spec-derive.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Structural validation lives here so the generator package can validate
|
|
9
|
+
* a spec without pulling in the inspector. Cross-registry validation
|
|
10
|
+
* (rule / knowledge / template / verification command resolution) lives
|
|
11
|
+
* in `@shrkcrft/inspector`.
|
|
12
|
+
*/
|
|
13
|
+
export const SPEC_SCHEMA_V1 = 'sharkcraft.spec/v1';
|
|
14
|
+
export const SPEC_EVENTS_SCHEMA_V1 = 'sharkcraft.spec-events/v1';
|
|
15
|
+
export var SpecStatus;
|
|
16
|
+
(function (SpecStatus) {
|
|
17
|
+
SpecStatus["Draft"] = "draft";
|
|
18
|
+
SpecStatus["Review"] = "review";
|
|
19
|
+
SpecStatus["Implementing"] = "implementing";
|
|
20
|
+
SpecStatus["Implemented"] = "implemented";
|
|
21
|
+
SpecStatus["Verified"] = "verified";
|
|
22
|
+
SpecStatus["Abandoned"] = "abandoned";
|
|
23
|
+
})(SpecStatus || (SpecStatus = {}));
|
|
24
|
+
export const SPEC_STATUS_VALUES = Object.freeze([
|
|
25
|
+
SpecStatus.Draft,
|
|
26
|
+
SpecStatus.Review,
|
|
27
|
+
SpecStatus.Implementing,
|
|
28
|
+
SpecStatus.Implemented,
|
|
29
|
+
SpecStatus.Verified,
|
|
30
|
+
SpecStatus.Abandoned,
|
|
31
|
+
]);
|
|
32
|
+
const KNOWN_TOP_LEVEL_KEYS = Object.freeze([
|
|
33
|
+
'schema',
|
|
34
|
+
'id',
|
|
35
|
+
'slug',
|
|
36
|
+
'title',
|
|
37
|
+
'status',
|
|
38
|
+
'createdAt',
|
|
39
|
+
'updatedAt',
|
|
40
|
+
'intent',
|
|
41
|
+
'motivation',
|
|
42
|
+
'acceptanceCriteria',
|
|
43
|
+
'affectedAreas',
|
|
44
|
+
'relevantRules',
|
|
45
|
+
'relevantKnowledge',
|
|
46
|
+
'relevantPaths',
|
|
47
|
+
'proposedTemplates',
|
|
48
|
+
'risks',
|
|
49
|
+
'outOfScope',
|
|
50
|
+
'externalLinks',
|
|
51
|
+
'boundariesCheck',
|
|
52
|
+
'verificationCommands',
|
|
53
|
+
'plan',
|
|
54
|
+
]);
|
|
55
|
+
export function knownTopLevelKeys() {
|
|
56
|
+
return KNOWN_TOP_LEVEL_KEYS;
|
|
57
|
+
}
|
|
58
|
+
/** Default spec body length cap, in bytes. */
|
|
59
|
+
export const DEFAULT_SPEC_BODY_MAX_BYTES = 16384;
|
|
60
|
+
export function validateSpecStructural(spec, body, options = {}) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
if (spec.schema !== SPEC_SCHEMA_V1) {
|
|
64
|
+
errors.push({
|
|
65
|
+
code: 'unsupported-schema',
|
|
66
|
+
severity: 'error',
|
|
67
|
+
field: 'schema',
|
|
68
|
+
message: `Spec schema must be ${SPEC_SCHEMA_V1} (got ${String(spec.schema)})`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!isSpecIdShape(spec.id)) {
|
|
72
|
+
errors.push({
|
|
73
|
+
code: 'invalid-spec-id',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
field: 'id',
|
|
76
|
+
message: `Spec id must match <YYYY-MM-DD>-<slug> (got "${spec.id}")`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!spec.slug || !/^[a-z0-9][a-z0-9-]*$/.test(spec.slug)) {
|
|
80
|
+
errors.push({
|
|
81
|
+
code: 'invalid-slug',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
field: 'slug',
|
|
84
|
+
message: `Slug must be kebab-case (got "${spec.slug}")`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!spec.title || spec.title.trim().length === 0) {
|
|
88
|
+
errors.push({
|
|
89
|
+
code: 'missing-title',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
field: 'title',
|
|
92
|
+
message: 'title must be a non-empty string',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!SPEC_STATUS_VALUES.includes(spec.status)) {
|
|
96
|
+
errors.push({
|
|
97
|
+
code: 'invalid-status',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
field: 'status',
|
|
100
|
+
message: `status must be one of ${SPEC_STATUS_VALUES.join(', ')} (got "${String(spec.status)}")`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!isIsoTimestamp(spec.createdAt)) {
|
|
104
|
+
errors.push({
|
|
105
|
+
code: 'invalid-created-at',
|
|
106
|
+
severity: 'error',
|
|
107
|
+
field: 'createdAt',
|
|
108
|
+
message: `createdAt must be an ISO-8601 timestamp (got "${spec.createdAt}")`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (!isIsoTimestamp(spec.updatedAt)) {
|
|
112
|
+
errors.push({
|
|
113
|
+
code: 'invalid-updated-at',
|
|
114
|
+
severity: 'error',
|
|
115
|
+
field: 'updatedAt',
|
|
116
|
+
message: `updatedAt must be an ISO-8601 timestamp (got "${spec.updatedAt}")`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (!spec.intent || spec.intent.trim().length === 0) {
|
|
120
|
+
errors.push({
|
|
121
|
+
code: 'missing-intent',
|
|
122
|
+
severity: 'error',
|
|
123
|
+
field: 'intent',
|
|
124
|
+
message: 'intent must be a non-empty string',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!spec.motivation || spec.motivation.trim().length === 0) {
|
|
128
|
+
errors.push({
|
|
129
|
+
code: 'missing-motivation',
|
|
130
|
+
severity: 'error',
|
|
131
|
+
field: 'motivation',
|
|
132
|
+
message: 'motivation must be a non-empty string',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(spec.acceptanceCriteria) || spec.acceptanceCriteria.length === 0) {
|
|
136
|
+
errors.push({
|
|
137
|
+
code: 'missing-acceptance-criteria',
|
|
138
|
+
severity: 'error',
|
|
139
|
+
field: 'acceptanceCriteria',
|
|
140
|
+
message: 'acceptanceCriteria must contain at least one entry',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (let i = 0; i < spec.acceptanceCriteria.length; i++) {
|
|
146
|
+
const ac = spec.acceptanceCriteria[i];
|
|
147
|
+
if (!ac.id) {
|
|
148
|
+
errors.push({
|
|
149
|
+
code: 'acceptance-missing-id',
|
|
150
|
+
severity: 'error',
|
|
151
|
+
field: `acceptanceCriteria[${i}].id`,
|
|
152
|
+
message: 'each acceptance criterion needs an id',
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(ac.id)) {
|
|
157
|
+
errors.push({
|
|
158
|
+
code: 'acceptance-duplicate-id',
|
|
159
|
+
severity: 'error',
|
|
160
|
+
field: `acceptanceCriteria[${i}].id`,
|
|
161
|
+
message: `duplicate acceptance criterion id "${ac.id}"`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
seen.add(ac.id);
|
|
165
|
+
if (!ac.text || ac.text.trim().length === 0) {
|
|
166
|
+
errors.push({
|
|
167
|
+
code: 'acceptance-missing-text',
|
|
168
|
+
severity: 'error',
|
|
169
|
+
field: `acceptanceCriteria[${i}].text`,
|
|
170
|
+
message: `acceptance criterion "${ac.id}" needs text`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (let i = 0; i < spec.proposedTemplates.length; i++) {
|
|
176
|
+
const t = spec.proposedTemplates[i];
|
|
177
|
+
if (!t.templateId) {
|
|
178
|
+
errors.push({
|
|
179
|
+
code: 'template-missing-id',
|
|
180
|
+
severity: 'error',
|
|
181
|
+
field: `proposedTemplates[${i}].templateId`,
|
|
182
|
+
message: 'each proposed template needs a templateId',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (let i = 0; i < spec.verificationCommands.length; i++) {
|
|
187
|
+
const v = spec.verificationCommands[i];
|
|
188
|
+
if (!v.id) {
|
|
189
|
+
errors.push({
|
|
190
|
+
code: 'verification-missing-id',
|
|
191
|
+
severity: 'error',
|
|
192
|
+
field: `verificationCommands[${i}].id`,
|
|
193
|
+
message: 'each verification command reference needs an id',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const k of spec.unknownKeys) {
|
|
198
|
+
warnings.push({
|
|
199
|
+
code: 'unknown-frontmatter-key',
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
field: k,
|
|
202
|
+
message: `Unknown frontmatter key "${k}" — preserved but unrecognised`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const maxBytes = options.bodyMaxBytes ?? DEFAULT_SPEC_BODY_MAX_BYTES;
|
|
206
|
+
const bodyBytes = Buffer.byteLength(body, 'utf8');
|
|
207
|
+
if (bodyBytes > maxBytes) {
|
|
208
|
+
warnings.push({
|
|
209
|
+
code: 'body-too-long',
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
field: 'body',
|
|
212
|
+
message: `spec body is ${bodyBytes} bytes; recommended max is ${maxBytes}. Specs must stay short — force structure.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return { errors, warnings };
|
|
216
|
+
}
|
|
217
|
+
export function isSpecIdShape(id) {
|
|
218
|
+
return /^\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]*$/.test(id);
|
|
219
|
+
}
|
|
220
|
+
function isIsoTimestamp(s) {
|
|
221
|
+
if (typeof s !== 'string' || s.length === 0)
|
|
222
|
+
return false;
|
|
223
|
+
const t = new Date(s).getTime();
|
|
224
|
+
return Number.isFinite(t);
|
|
225
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a `spec.md` scaffold from grounded inputs.
|
|
3
|
+
*
|
|
4
|
+
* The engine does NOT write the prose. The scaffold leaves
|
|
5
|
+
* intent / motivation / acceptanceCriteria / risks deliberately
|
|
6
|
+
* empty (with placeholder text the human / agent fills in). The
|
|
7
|
+
* grounding fields (relevantRules / relevantKnowledge /
|
|
8
|
+
* relevantPaths / proposedTemplates / verificationCommands) are
|
|
9
|
+
* pre-populated from the recommender outputs the caller provides.
|
|
10
|
+
*
|
|
11
|
+
* Output is the markdown text of `spec.md`. Pure function; no IO.
|
|
12
|
+
*/
|
|
13
|
+
export interface IRenderSpecMdInput {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly slug: string;
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly createdAt: string;
|
|
18
|
+
readonly updatedAt: string;
|
|
19
|
+
readonly issue?: string | null;
|
|
20
|
+
readonly relevantRules: readonly string[];
|
|
21
|
+
readonly relevantKnowledge: readonly string[];
|
|
22
|
+
readonly relevantPaths: readonly string[];
|
|
23
|
+
readonly affectedPackages: readonly string[];
|
|
24
|
+
readonly proposedTemplates: ReadonlyArray<{
|
|
25
|
+
readonly templateId: string;
|
|
26
|
+
readonly variables: Readonly<Record<string, string>>;
|
|
27
|
+
readonly note?: string;
|
|
28
|
+
}>;
|
|
29
|
+
readonly verificationCommandIds: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function renderSpecMd(input: IRenderSpecMdInput): string;
|
|
32
|
+
//# sourceMappingURL=spec-scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-scaffold.d.ts","sourceRoot":"","sources":["../../src/spec/spec-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC;QACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAiF9D"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a `spec.md` scaffold from grounded inputs.
|
|
3
|
+
*
|
|
4
|
+
* The engine does NOT write the prose. The scaffold leaves
|
|
5
|
+
* intent / motivation / acceptanceCriteria / risks deliberately
|
|
6
|
+
* empty (with placeholder text the human / agent fills in). The
|
|
7
|
+
* grounding fields (relevantRules / relevantKnowledge /
|
|
8
|
+
* relevantPaths / proposedTemplates / verificationCommands) are
|
|
9
|
+
* pre-populated from the recommender outputs the caller provides.
|
|
10
|
+
*
|
|
11
|
+
* Output is the markdown text of `spec.md`. Pure function; no IO.
|
|
12
|
+
*/
|
|
13
|
+
import { SPEC_SCHEMA_V1, SpecStatus } from "./spec-model.js";
|
|
14
|
+
export function renderSpecMd(input) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push('---');
|
|
17
|
+
lines.push(`schema: ${SPEC_SCHEMA_V1}`);
|
|
18
|
+
lines.push(`id: ${input.id}`);
|
|
19
|
+
lines.push(`slug: ${input.slug}`);
|
|
20
|
+
lines.push(`title: ${quoteIfNeeded(input.title)}`);
|
|
21
|
+
lines.push(`status: ${SpecStatus.Draft}`);
|
|
22
|
+
lines.push(`createdAt: ${input.createdAt}`);
|
|
23
|
+
lines.push(`updatedAt: ${input.updatedAt}`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push('intent: |');
|
|
26
|
+
lines.push(' TODO: one-paragraph statement of what is being built.');
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push('motivation: |');
|
|
29
|
+
lines.push(' TODO: why now. The forcing function. Cross-link to issue if any.');
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('acceptanceCriteria:');
|
|
32
|
+
lines.push(' - id: ac-1');
|
|
33
|
+
lines.push(' text: TODO: replace with a concrete, testable acceptance criterion.');
|
|
34
|
+
lines.push(' verifiedBy:');
|
|
35
|
+
lines.push(' - tests');
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push('affectedAreas:');
|
|
38
|
+
lines.push(' files:');
|
|
39
|
+
lines.push(' packages:');
|
|
40
|
+
for (const p of input.affectedPackages)
|
|
41
|
+
lines.push(` - ${quoteIfNeeded(p)}`);
|
|
42
|
+
lines.push(' layers:');
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push('relevantRules:');
|
|
45
|
+
for (const r of input.relevantRules)
|
|
46
|
+
lines.push(` - ${quoteIfNeeded(r)}`);
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push('relevantKnowledge:');
|
|
49
|
+
for (const k of input.relevantKnowledge)
|
|
50
|
+
lines.push(` - ${quoteIfNeeded(k)}`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push('relevantPaths:');
|
|
53
|
+
for (const p of input.relevantPaths)
|
|
54
|
+
lines.push(` - ${quoteIfNeeded(p)}`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('proposedTemplates:');
|
|
57
|
+
for (const t of input.proposedTemplates) {
|
|
58
|
+
lines.push(` - templateId: ${quoteIfNeeded(t.templateId)}`);
|
|
59
|
+
lines.push(' variables:');
|
|
60
|
+
for (const [k, v] of Object.entries(t.variables)) {
|
|
61
|
+
lines.push(` ${k}: ${quoteIfNeeded(v)}`);
|
|
62
|
+
}
|
|
63
|
+
if (t.note !== undefined) {
|
|
64
|
+
lines.push(` note: ${quoteIfNeeded(t.note)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('risks:');
|
|
69
|
+
lines.push(' - id: r-1');
|
|
70
|
+
lines.push(' text: TODO: identify a real risk this change introduces.');
|
|
71
|
+
lines.push(' mitigation: TODO: how the change mitigates it.');
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push('outOfScope:');
|
|
74
|
+
lines.push(' - TODO: enumerate explicit non-goals.');
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('externalLinks:');
|
|
77
|
+
lines.push(` issue: ${input.issue ? quoteIfNeeded(input.issue) : 'null'}`);
|
|
78
|
+
lines.push(' pr: null');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('boundariesCheck:');
|
|
81
|
+
lines.push(' predicted:');
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push('verificationCommands:');
|
|
84
|
+
for (const id of input.verificationCommandIds) {
|
|
85
|
+
lines.push(` - id: ${quoteIfNeeded(id)}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('---');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(`# ${input.title}`);
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('Body is free-form markdown. Architecture sketches, decision notes, design hazards.');
|
|
92
|
+
lines.push('Specs must stay short — force structure. Keep this section under the configured byte cap.');
|
|
93
|
+
lines.push('');
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
function quoteIfNeeded(s) {
|
|
97
|
+
// Bare strings are safe iff they contain no `:`, `#`, leading `-`,
|
|
98
|
+
// and only printable ASCII besides whitespace.
|
|
99
|
+
if (s.length === 0)
|
|
100
|
+
return '""';
|
|
101
|
+
if (/[:#\n\r\t]/.test(s))
|
|
102
|
+
return JSON.stringify(s);
|
|
103
|
+
if (s.startsWith('-') || s.startsWith('?') || s.startsWith('!'))
|
|
104
|
+
return JSON.stringify(s);
|
|
105
|
+
return s;
|
|
106
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type IFileChange } from './file-change.js';
|
|
2
|
+
import type { IGenerationPlan } from './generation-plan.js';
|
|
3
|
+
import type { ISavedPlan } from './saved-plan.js';
|
|
4
|
+
export declare const SYNTHETIC_TEMPLATE_PREFIX = "__";
|
|
5
|
+
export declare function isSyntheticTemplateId(templateId: string): boolean;
|
|
6
|
+
export declare function evaluateSavedPlanInPlace(plan: ISavedPlan, projectRoot: string): IGenerationPlan;
|
|
7
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
8
|
+
import type { IGenerationSummary } from './generation-plan.js';
|
|
9
|
+
export interface ISyntheticWriteResult {
|
|
10
|
+
summary: IGenerationSummary;
|
|
11
|
+
written: readonly IFileChange[];
|
|
12
|
+
}
|
|
13
|
+
export declare function writeSyntheticPlan(plan: IGenerationPlan): Result<ISyntheticWriteResult, AppError>;
|
|
14
|
+
//# sourceMappingURL=synthetic-plan.d.ts.map
|
|
@@ -0,0 +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"}
|