@warnyin/sdlc 0.1.0 → 0.2.0
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/CHANGELOG.md +53 -19
- package/LICENSE +21 -21
- package/README.md +83 -74
- package/bin/cli.mjs +111 -31
- package/bin/detect.mjs +51 -0
- package/bin/multiselect.mjs +147 -0
- package/bin/ui.mjs +78 -0
- package/lib/caps.mjs +45 -45
- package/lib/config.mjs +41 -41
- package/lib/delta.mjs +160 -160
- package/lib/frontmatter.mjs +59 -59
- package/lib/glob.mjs +29 -29
- package/lib/manifest.mjs +99 -99
- package/lib/observe.mjs +174 -174
- package/lib/settings-merge.mjs +63 -63
- package/lib/usage.mjs +46 -46
- package/lib/validate.mjs +186 -186
- package/package.json +42 -42
- package/payload/adapters/agents-md.md +8 -8
- package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
- package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
- package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
- package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
- package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
- package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
- package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
- package/payload/adapters/claude/agents/sdlc-security.md +12 -12
- package/payload/adapters/claude/commands/sdlc/auto.md +5 -5
- package/payload/adapters/claude/commands/sdlc/build.md +5 -5
- package/payload/adapters/claude/commands/sdlc/contract.md +5 -5
- package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
- package/payload/adapters/claude/commands/sdlc/design.md +5 -5
- package/payload/adapters/claude/commands/sdlc/init.md +4 -4
- package/payload/adapters/claude/commands/sdlc/new.md +5 -5
- package/payload/adapters/claude/commands/sdlc/next.md +4 -4
- package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
- package/payload/adapters/claude/commands/sdlc/review.md +5 -5
- package/payload/adapters/claude/commands/sdlc/ship.md +5 -5
- package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
- package/payload/adapters/claude/commands/sdlc/verify.md +5 -5
- package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
- package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +33 -33
- package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +26 -26
- package/payload/adapters/cline.md +8 -8
- package/payload/adapters/copilot.md +8 -8
- package/payload/adapters/cursor.mdc +7 -7
- package/payload/adapters/gemini.md +8 -8
- package/payload/adapters/windsurf.md +4 -4
- package/payload/hooks/_shared.mjs +154 -141
- package/payload/hooks/guard-writes.mjs +83 -83
- package/payload/hooks/inject-context.mjs +55 -55
- package/payload/hooks/journal.mjs +58 -58
- package/payload/hooks/session-summary.mjs +50 -50
- package/payload/hooks/validate-artifact.mjs +80 -80
- package/payload/playbook/README.md +30 -30
- package/payload/playbook/auto.md +21 -21
- package/payload/playbook/build.md +23 -23
- package/payload/playbook/context.md +26 -26
- package/payload/playbook/contract.md +23 -23
- package/payload/playbook/converge.md +19 -19
- package/payload/playbook/design.md +20 -20
- package/payload/playbook/init.md +22 -22
- package/payload/playbook/new.md +22 -22
- package/payload/playbook/next.md +12 -12
- package/payload/playbook/observe.md +20 -20
- package/payload/playbook/principles.md +28 -28
- package/payload/playbook/review.md +17 -17
- package/payload/playbook/routing.md +19 -19
- package/payload/playbook/rules-card.md +16 -16
- package/payload/playbook/ship.md +24 -24
- package/payload/playbook/steer.md +21 -21
- package/payload/playbook/verify.md +24 -24
- package/payload/templates/change-deep.md +29 -29
- package/payload/templates/change-standard.md +28 -28
- package/payload/templates/change-vibe.md +19 -19
- package/payload/templates/config.yaml +8 -8
- package/payload/templates/constitution.md +14 -14
- package/payload/templates/contract-evals.md +9 -9
- package/payload/templates/contract-tests.md +9 -9
- package/payload/templates/harness.md +33 -33
- package/payload/templates/spec.md +14 -14
- package/payload/templates/steering.md +9 -9
- package/scripts/validate.mjs +47 -38
package/lib/delta.mjs
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
// Delta-spec grammar (parser keys are frozen English regardless of any
|
|
2
|
-
// UI language):
|
|
3
|
-
//
|
|
4
|
-
// ## Delta: <capability>
|
|
5
|
-
// ### ADDED Requirement: <name>
|
|
6
|
-
// The system SHALL <behavior>.
|
|
7
|
-
// #### Scenario: <name>
|
|
8
|
-
// - WHEN <condition>
|
|
9
|
-
// - THEN <outcome>
|
|
10
|
-
// ### MODIFIED Requirement: <existing name> ← full replacement body
|
|
11
|
-
// ### REMOVED Requirement: <existing name>
|
|
12
|
-
//
|
|
13
|
-
// Requirement heading text is the identity key (OpenSpec convention).
|
|
14
|
-
// `mergeDelta` applies ops mechanically to a living spec; a missing key on
|
|
15
|
-
// MODIFIED/REMOVED is a hard error — never merge silently.
|
|
16
|
-
|
|
17
|
-
const DELTA_HEAD = /^## Delta:\s*(.+?)\s*$/;
|
|
18
|
-
const OP_HEAD = /^### (ADDED|MODIFIED|REMOVED) Requirement:\s*(.+?)\s*$/;
|
|
19
|
-
const BAD_OP_HEAD = /^### (\w+) Requirement:/;
|
|
20
|
-
const SPEC_REQ_HEAD = /^### Requirement:\s*(.+?)\s*$/;
|
|
21
|
-
|
|
22
|
-
export function parseDelta(changeText) {
|
|
23
|
-
const lines = (changeText ?? '').split(/\r?\n/);
|
|
24
|
-
const deltas = [];
|
|
25
|
-
const errors = [];
|
|
26
|
-
let current = null; // { capability, ops }
|
|
27
|
-
let currentOp = null; // { op, name, bodyLines }
|
|
28
|
-
|
|
29
|
-
const closeOp = () => {
|
|
30
|
-
if (!currentOp) return;
|
|
31
|
-
const body = currentOp.bodyLines.join('\n').trim();
|
|
32
|
-
if (currentOp.op !== 'REMOVED' && body === '') {
|
|
33
|
-
errors.push(`${currentOp.op} Requirement "${currentOp.name}" has an empty body`);
|
|
34
|
-
}
|
|
35
|
-
current.ops.push({ op: currentOp.op, name: currentOp.name, body });
|
|
36
|
-
currentOp = null;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
const deltaMatch = line.match(DELTA_HEAD);
|
|
41
|
-
if (deltaMatch) {
|
|
42
|
-
closeOp();
|
|
43
|
-
// Repeated `## Delta: <cap>` blocks merge into one entry — otherwise the
|
|
44
|
-
// last block would silently overwrite the first at ship time.
|
|
45
|
-
const existing = deltas.find((d) => d.capability.toLowerCase() === deltaMatch[1].toLowerCase());
|
|
46
|
-
current = existing ?? { capability: deltaMatch[1], ops: [] };
|
|
47
|
-
if (!existing) deltas.push(current);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (/^## /.test(line) && !deltaMatch) {
|
|
51
|
-
closeOp();
|
|
52
|
-
current = null; // left the delta section
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (!current) continue;
|
|
56
|
-
|
|
57
|
-
const opMatch = line.match(OP_HEAD);
|
|
58
|
-
if (opMatch) {
|
|
59
|
-
closeOp();
|
|
60
|
-
currentOp = { op: opMatch[1], name: opMatch[2], bodyLines: [] };
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const badOp = line.match(BAD_OP_HEAD);
|
|
64
|
-
if (badOp && !opMatch) {
|
|
65
|
-
errors.push(`Unknown delta operation "${badOp[1]}" (use ADDED, MODIFIED, or REMOVED)`);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (currentOp) currentOp.bodyLines.push(line);
|
|
69
|
-
}
|
|
70
|
-
closeOp();
|
|
71
|
-
|
|
72
|
-
for (const d of deltas) {
|
|
73
|
-
const seen = new Set();
|
|
74
|
-
for (const op of d.ops) {
|
|
75
|
-
const key = op.name.toLowerCase();
|
|
76
|
-
if (seen.has(key)) errors.push(`Duplicate requirement "${op.name}" in Delta: ${d.capability}`);
|
|
77
|
-
seen.add(key);
|
|
78
|
-
}
|
|
79
|
-
if (d.ops.length === 0) errors.push(`Delta: ${d.capability} declares no requirement operations`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { deltas, errors };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function parseSpec(specText) {
|
|
86
|
-
const lines = (specText ?? '').split(/\r?\n/);
|
|
87
|
-
const requirements = [];
|
|
88
|
-
const preamble = [];
|
|
89
|
-
let current = null;
|
|
90
|
-
|
|
91
|
-
for (const line of lines) {
|
|
92
|
-
const reqMatch = line.match(SPEC_REQ_HEAD);
|
|
93
|
-
if (reqMatch) {
|
|
94
|
-
current = { name: reqMatch[1], bodyLines: [] };
|
|
95
|
-
requirements.push(current);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (current) current.bodyLines.push(line);
|
|
99
|
-
else preamble.push(line);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
preamble: preamble.join('\n').replace(/\n+$/, ''),
|
|
104
|
-
requirements: requirements.map((r) => ({
|
|
105
|
-
name: r.name,
|
|
106
|
-
body: r.bodyLines.join('\n').trim(),
|
|
107
|
-
})),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function renderSpec({ preamble, requirements }) {
|
|
112
|
-
const parts = [preamble.replace(/\n+$/, '')];
|
|
113
|
-
for (const r of requirements) {
|
|
114
|
-
parts.push(`\n### Requirement: ${r.name}\n${r.body}`);
|
|
115
|
-
}
|
|
116
|
-
return parts.join('\n').replace(/\n+$/, '\n');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function newSpecPreamble(capability) {
|
|
120
|
-
return [
|
|
121
|
-
`# Spec: ${capability}`,
|
|
122
|
-
'',
|
|
123
|
-
'## Purpose',
|
|
124
|
-
'<!-- one or two lines; commands grep this header first (progressive disclosure) -->',
|
|
125
|
-
'',
|
|
126
|
-
'## Requirements',
|
|
127
|
-
].join('\n');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Apply one capability's ops to a living spec (or null to create it).
|
|
131
|
-
// Returns { ok, content, errors }.
|
|
132
|
-
export function mergeDelta(specText, ops, capability) {
|
|
133
|
-
const errors = [];
|
|
134
|
-
const spec = specText == null
|
|
135
|
-
? { preamble: newSpecPreamble(capability), requirements: [] }
|
|
136
|
-
: parseSpec(specText);
|
|
137
|
-
|
|
138
|
-
const byName = new Map(spec.requirements.map((r) => [r.name.toLowerCase(), r]));
|
|
139
|
-
|
|
140
|
-
for (const { op, name, body } of ops) {
|
|
141
|
-
const key = name.toLowerCase();
|
|
142
|
-
const existing = byName.get(key);
|
|
143
|
-
if (op === 'ADDED') {
|
|
144
|
-
if (existing) { errors.push(`ADDED Requirement "${name}" already exists in spec "${capability}"`); continue; }
|
|
145
|
-
const req = { name, body };
|
|
146
|
-
spec.requirements.push(req);
|
|
147
|
-
byName.set(key, req);
|
|
148
|
-
} else if (op === 'MODIFIED') {
|
|
149
|
-
if (!existing) { errors.push(`MODIFIED Requirement "${name}" not found in spec "${capability}"`); continue; }
|
|
150
|
-
existing.body = body;
|
|
151
|
-
} else if (op === 'REMOVED') {
|
|
152
|
-
if (!existing) { errors.push(`REMOVED Requirement "${name}" not found in spec "${capability}"`); continue; }
|
|
153
|
-
spec.requirements = spec.requirements.filter((r) => r.name.toLowerCase() !== key);
|
|
154
|
-
byName.delete(key);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (errors.length) return { ok: false, content: null, errors };
|
|
159
|
-
return { ok: true, content: renderSpec(spec), errors: [] };
|
|
160
|
-
}
|
|
1
|
+
// Delta-spec grammar (parser keys are frozen English regardless of any
|
|
2
|
+
// UI language):
|
|
3
|
+
//
|
|
4
|
+
// ## Delta: <capability>
|
|
5
|
+
// ### ADDED Requirement: <name>
|
|
6
|
+
// The system SHALL <behavior>.
|
|
7
|
+
// #### Scenario: <name>
|
|
8
|
+
// - WHEN <condition>
|
|
9
|
+
// - THEN <outcome>
|
|
10
|
+
// ### MODIFIED Requirement: <existing name> ← full replacement body
|
|
11
|
+
// ### REMOVED Requirement: <existing name>
|
|
12
|
+
//
|
|
13
|
+
// Requirement heading text is the identity key (OpenSpec convention).
|
|
14
|
+
// `mergeDelta` applies ops mechanically to a living spec; a missing key on
|
|
15
|
+
// MODIFIED/REMOVED is a hard error — never merge silently.
|
|
16
|
+
|
|
17
|
+
const DELTA_HEAD = /^## Delta:\s*(.+?)\s*$/;
|
|
18
|
+
const OP_HEAD = /^### (ADDED|MODIFIED|REMOVED) Requirement:\s*(.+?)\s*$/;
|
|
19
|
+
const BAD_OP_HEAD = /^### (\w+) Requirement:/;
|
|
20
|
+
const SPEC_REQ_HEAD = /^### Requirement:\s*(.+?)\s*$/;
|
|
21
|
+
|
|
22
|
+
export function parseDelta(changeText) {
|
|
23
|
+
const lines = (changeText ?? '').split(/\r?\n/);
|
|
24
|
+
const deltas = [];
|
|
25
|
+
const errors = [];
|
|
26
|
+
let current = null; // { capability, ops }
|
|
27
|
+
let currentOp = null; // { op, name, bodyLines }
|
|
28
|
+
|
|
29
|
+
const closeOp = () => {
|
|
30
|
+
if (!currentOp) return;
|
|
31
|
+
const body = currentOp.bodyLines.join('\n').trim();
|
|
32
|
+
if (currentOp.op !== 'REMOVED' && body === '') {
|
|
33
|
+
errors.push(`${currentOp.op} Requirement "${currentOp.name}" has an empty body`);
|
|
34
|
+
}
|
|
35
|
+
current.ops.push({ op: currentOp.op, name: currentOp.name, body });
|
|
36
|
+
currentOp = null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const deltaMatch = line.match(DELTA_HEAD);
|
|
41
|
+
if (deltaMatch) {
|
|
42
|
+
closeOp();
|
|
43
|
+
// Repeated `## Delta: <cap>` blocks merge into one entry — otherwise the
|
|
44
|
+
// last block would silently overwrite the first at ship time.
|
|
45
|
+
const existing = deltas.find((d) => d.capability.toLowerCase() === deltaMatch[1].toLowerCase());
|
|
46
|
+
current = existing ?? { capability: deltaMatch[1], ops: [] };
|
|
47
|
+
if (!existing) deltas.push(current);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^## /.test(line) && !deltaMatch) {
|
|
51
|
+
closeOp();
|
|
52
|
+
current = null; // left the delta section
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!current) continue;
|
|
56
|
+
|
|
57
|
+
const opMatch = line.match(OP_HEAD);
|
|
58
|
+
if (opMatch) {
|
|
59
|
+
closeOp();
|
|
60
|
+
currentOp = { op: opMatch[1], name: opMatch[2], bodyLines: [] };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const badOp = line.match(BAD_OP_HEAD);
|
|
64
|
+
if (badOp && !opMatch) {
|
|
65
|
+
errors.push(`Unknown delta operation "${badOp[1]}" (use ADDED, MODIFIED, or REMOVED)`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (currentOp) currentOp.bodyLines.push(line);
|
|
69
|
+
}
|
|
70
|
+
closeOp();
|
|
71
|
+
|
|
72
|
+
for (const d of deltas) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const op of d.ops) {
|
|
75
|
+
const key = op.name.toLowerCase();
|
|
76
|
+
if (seen.has(key)) errors.push(`Duplicate requirement "${op.name}" in Delta: ${d.capability}`);
|
|
77
|
+
seen.add(key);
|
|
78
|
+
}
|
|
79
|
+
if (d.ops.length === 0) errors.push(`Delta: ${d.capability} declares no requirement operations`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { deltas, errors };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseSpec(specText) {
|
|
86
|
+
const lines = (specText ?? '').split(/\r?\n/);
|
|
87
|
+
const requirements = [];
|
|
88
|
+
const preamble = [];
|
|
89
|
+
let current = null;
|
|
90
|
+
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const reqMatch = line.match(SPEC_REQ_HEAD);
|
|
93
|
+
if (reqMatch) {
|
|
94
|
+
current = { name: reqMatch[1], bodyLines: [] };
|
|
95
|
+
requirements.push(current);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (current) current.bodyLines.push(line);
|
|
99
|
+
else preamble.push(line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
preamble: preamble.join('\n').replace(/\n+$/, ''),
|
|
104
|
+
requirements: requirements.map((r) => ({
|
|
105
|
+
name: r.name,
|
|
106
|
+
body: r.bodyLines.join('\n').trim(),
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderSpec({ preamble, requirements }) {
|
|
112
|
+
const parts = [preamble.replace(/\n+$/, '')];
|
|
113
|
+
for (const r of requirements) {
|
|
114
|
+
parts.push(`\n### Requirement: ${r.name}\n${r.body}`);
|
|
115
|
+
}
|
|
116
|
+
return parts.join('\n').replace(/\n+$/, '\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function newSpecPreamble(capability) {
|
|
120
|
+
return [
|
|
121
|
+
`# Spec: ${capability}`,
|
|
122
|
+
'',
|
|
123
|
+
'## Purpose',
|
|
124
|
+
'<!-- one or two lines; commands grep this header first (progressive disclosure) -->',
|
|
125
|
+
'',
|
|
126
|
+
'## Requirements',
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Apply one capability's ops to a living spec (or null to create it).
|
|
131
|
+
// Returns { ok, content, errors }.
|
|
132
|
+
export function mergeDelta(specText, ops, capability) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
const spec = specText == null
|
|
135
|
+
? { preamble: newSpecPreamble(capability), requirements: [] }
|
|
136
|
+
: parseSpec(specText);
|
|
137
|
+
|
|
138
|
+
const byName = new Map(spec.requirements.map((r) => [r.name.toLowerCase(), r]));
|
|
139
|
+
|
|
140
|
+
for (const { op, name, body } of ops) {
|
|
141
|
+
const key = name.toLowerCase();
|
|
142
|
+
const existing = byName.get(key);
|
|
143
|
+
if (op === 'ADDED') {
|
|
144
|
+
if (existing) { errors.push(`ADDED Requirement "${name}" already exists in spec "${capability}"`); continue; }
|
|
145
|
+
const req = { name, body };
|
|
146
|
+
spec.requirements.push(req);
|
|
147
|
+
byName.set(key, req);
|
|
148
|
+
} else if (op === 'MODIFIED') {
|
|
149
|
+
if (!existing) { errors.push(`MODIFIED Requirement "${name}" not found in spec "${capability}"`); continue; }
|
|
150
|
+
existing.body = body;
|
|
151
|
+
} else if (op === 'REMOVED') {
|
|
152
|
+
if (!existing) { errors.push(`REMOVED Requirement "${name}" not found in spec "${capability}"`); continue; }
|
|
153
|
+
spec.requirements = spec.requirements.filter((r) => r.name.toLowerCase() !== key);
|
|
154
|
+
byName.delete(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (errors.length) return { ok: false, content: null, errors };
|
|
159
|
+
return { ok: true, content: renderSpec(spec), errors: [] };
|
|
160
|
+
}
|
package/lib/frontmatter.mjs
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
// Minimal YAML-frontmatter reader (zero-dep). Supports the subset this
|
|
2
|
-
// framework writes: strings, numbers, booleans, inline arrays, and
|
|
3
|
-
// simple `- item` lists. Anything fancier is a validation error upstream.
|
|
4
|
-
|
|
5
|
-
const FENCE = '---';
|
|
6
|
-
|
|
7
|
-
export function parseFrontmatter(text) {
|
|
8
|
-
if (typeof text !== 'string') return { data: {}, body: '' };
|
|
9
|
-
const lines = text.split(/\r?\n/);
|
|
10
|
-
if (lines[0]?.trim() !== FENCE) return { data: {}, body: text };
|
|
11
|
-
|
|
12
|
-
let end = -1;
|
|
13
|
-
for (let i = 1; i < lines.length; i++) {
|
|
14
|
-
if (lines[i].trim() === FENCE) { end = i; break; }
|
|
15
|
-
}
|
|
16
|
-
if (end === -1) return { data: {}, body: text };
|
|
17
|
-
|
|
18
|
-
const data = {};
|
|
19
|
-
let currentListKey = null;
|
|
20
|
-
for (let i = 1; i < end; i++) {
|
|
21
|
-
const raw = lines[i];
|
|
22
|
-
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
|
23
|
-
|
|
24
|
-
const listItem = raw.match(/^\s+-\s+(.*)$/);
|
|
25
|
-
if (listItem && currentListKey) {
|
|
26
|
-
data[currentListKey].push(coerce(listItem[1].trim()));
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
31
|
-
if (!kv) continue;
|
|
32
|
-
const [, key, rawValue] = kv;
|
|
33
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
34
|
-
const value = rawValue.trim();
|
|
35
|
-
if (value === '') {
|
|
36
|
-
data[key] = [];
|
|
37
|
-
currentListKey = key;
|
|
38
|
-
} else {
|
|
39
|
-
data[key] = coerce(value);
|
|
40
|
-
currentListKey = null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { data, body: lines.slice(end + 1).join('\n') };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function coerce(value) {
|
|
48
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
49
|
-
const inner = value.slice(1, -1).trim();
|
|
50
|
-
if (!inner) return [];
|
|
51
|
-
return inner.split(',').map((v) => coerce(v.trim()));
|
|
52
|
-
}
|
|
53
|
-
const unquoted = value.replace(/^["']|["']$/g, '');
|
|
54
|
-
if (unquoted !== value) return unquoted;
|
|
55
|
-
if (value === 'true') return true;
|
|
56
|
-
if (value === 'false') return false;
|
|
57
|
-
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
1
|
+
// Minimal YAML-frontmatter reader (zero-dep). Supports the subset this
|
|
2
|
+
// framework writes: strings, numbers, booleans, inline arrays, and
|
|
3
|
+
// simple `- item` lists. Anything fancier is a validation error upstream.
|
|
4
|
+
|
|
5
|
+
const FENCE = '---';
|
|
6
|
+
|
|
7
|
+
export function parseFrontmatter(text) {
|
|
8
|
+
if (typeof text !== 'string') return { data: {}, body: '' };
|
|
9
|
+
const lines = text.split(/\r?\n/);
|
|
10
|
+
if (lines[0]?.trim() !== FENCE) return { data: {}, body: text };
|
|
11
|
+
|
|
12
|
+
let end = -1;
|
|
13
|
+
for (let i = 1; i < lines.length; i++) {
|
|
14
|
+
if (lines[i].trim() === FENCE) { end = i; break; }
|
|
15
|
+
}
|
|
16
|
+
if (end === -1) return { data: {}, body: text };
|
|
17
|
+
|
|
18
|
+
const data = {};
|
|
19
|
+
let currentListKey = null;
|
|
20
|
+
for (let i = 1; i < end; i++) {
|
|
21
|
+
const raw = lines[i];
|
|
22
|
+
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
|
23
|
+
|
|
24
|
+
const listItem = raw.match(/^\s+-\s+(.*)$/);
|
|
25
|
+
if (listItem && currentListKey) {
|
|
26
|
+
data[currentListKey].push(coerce(listItem[1].trim()));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
31
|
+
if (!kv) continue;
|
|
32
|
+
const [, key, rawValue] = kv;
|
|
33
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
34
|
+
const value = rawValue.trim();
|
|
35
|
+
if (value === '') {
|
|
36
|
+
data[key] = [];
|
|
37
|
+
currentListKey = key;
|
|
38
|
+
} else {
|
|
39
|
+
data[key] = coerce(value);
|
|
40
|
+
currentListKey = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { data, body: lines.slice(end + 1).join('\n') };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function coerce(value) {
|
|
48
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
49
|
+
const inner = value.slice(1, -1).trim();
|
|
50
|
+
if (!inner) return [];
|
|
51
|
+
return inner.split(',').map((v) => coerce(v.trim()));
|
|
52
|
+
}
|
|
53
|
+
const unquoted = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
if (unquoted !== value) return unquoted;
|
|
55
|
+
if (value === 'true') return true;
|
|
56
|
+
if (value === 'false') return false;
|
|
57
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
package/lib/glob.mjs
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
// Tiny glob matcher for steering pathMatch patterns. Supports the subset we
|
|
2
|
-
// document: `**` (any depth), `*` (within a segment), literal text.
|
|
3
|
-
// Paths are compared as POSIX, relative to the project root.
|
|
4
|
-
|
|
5
|
-
export function globToRegExp(glob) {
|
|
6
|
-
let re = '';
|
|
7
|
-
for (let i = 0; i < glob.length; i++) {
|
|
8
|
-
const c = glob[i];
|
|
9
|
-
if (c === '*') {
|
|
10
|
-
if (glob[i + 1] === '*') {
|
|
11
|
-
// `**/` or trailing `**` — match any depth including nothing
|
|
12
|
-
if (glob[i + 2] === '/') { re += '(?:[^/]+/)*'; i += 2; }
|
|
13
|
-
else { re += '.*'; i += 1; }
|
|
14
|
-
} else {
|
|
15
|
-
re += '[^/]*';
|
|
16
|
-
}
|
|
17
|
-
} else if ('.+^$()[]{}|\\?'.includes(c)) {
|
|
18
|
-
re += '\\' + c;
|
|
19
|
-
} else {
|
|
20
|
-
re += c;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return new RegExp(`^${re}$`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function matchGlob(relPosixPath, patterns) {
|
|
27
|
-
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
28
|
-
return list.some((p) => globToRegExp(String(p)).test(relPosixPath));
|
|
29
|
-
}
|
|
1
|
+
// Tiny glob matcher for steering pathMatch patterns. Supports the subset we
|
|
2
|
+
// document: `**` (any depth), `*` (within a segment), literal text.
|
|
3
|
+
// Paths are compared as POSIX, relative to the project root.
|
|
4
|
+
|
|
5
|
+
export function globToRegExp(glob) {
|
|
6
|
+
let re = '';
|
|
7
|
+
for (let i = 0; i < glob.length; i++) {
|
|
8
|
+
const c = glob[i];
|
|
9
|
+
if (c === '*') {
|
|
10
|
+
if (glob[i + 1] === '*') {
|
|
11
|
+
// `**/` or trailing `**` — match any depth including nothing
|
|
12
|
+
if (glob[i + 2] === '/') { re += '(?:[^/]+/)*'; i += 2; }
|
|
13
|
+
else { re += '.*'; i += 1; }
|
|
14
|
+
} else {
|
|
15
|
+
re += '[^/]*';
|
|
16
|
+
}
|
|
17
|
+
} else if ('.+^$()[]{}|\\?'.includes(c)) {
|
|
18
|
+
re += '\\' + c;
|
|
19
|
+
} else {
|
|
20
|
+
re += c;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return new RegExp(`^${re}$`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function matchGlob(relPosixPath, patterns) {
|
|
27
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
28
|
+
return list.some((p) => globToRegExp(String(p)).test(relPosixPath));
|
|
29
|
+
}
|