@supersuit/superskill 0.2.0 → 0.2.2
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 +28 -0
- package/SPEC.md +1 -1
- package/package.json +1 -1
- package/src/frontmatter.mjs +102 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
- An inline flow map (`scope: { form: essay, audience: builders, purpose: persuade }`, `check: {
|
|
6
|
+
station: every segment carries a label }`) now reads as an object instead of the whole `{ ...
|
|
7
|
+
}` coming back as a string. Nested inline maps and inline lists inside inline maps work too
|
|
8
|
+
(`speech: { uses: [a, b], never: [c] }`).
|
|
9
|
+
- An inline flow list followed by a same-line comment (`conditions: [r1, r2, r3] # 5 to 10
|
|
10
|
+
ids`) now reads as a list. Before this, the inline-list check required the raw value to END in
|
|
11
|
+
`]`, and a trailing comment broke that, so the whole line came back as a string.
|
|
12
|
+
- Both are read by a quote-aware character scanner rather than a naive split, so a comma,
|
|
13
|
+
bracket, brace or hash inside a quoted value inside a flow collection stays text
|
|
14
|
+
(`{ note: "a, b] } # c" }`). Malformed flow syntax (unbalanced brackets) still never throws: it
|
|
15
|
+
falls back to the raw string, same as an unrecognized value always has.
|
|
16
|
+
- A block list item that is itself a bare inline flow map or list (`- { station: fine, severity:
|
|
17
|
+
fail }`, `- [a, b]`) now reads as an object or a list. Before this, the KEY regex that decides
|
|
18
|
+
whether `- key: value` opens a block submap matched on the first colon inside the braces
|
|
19
|
+
(reading `"{ station"` as the key), so the item came back as `{ "{ station": "fine, severity:
|
|
20
|
+
fail }" }`. `- key: { ... }` still opens a block submap as before.
|
|
21
|
+
|
|
22
|
+
## 0.2.1 (2026-09-28)
|
|
23
|
+
|
|
24
|
+
- An unquoted value that is only a comment now reads as empty, which is what YAML means. Before
|
|
25
|
+
this, `source: # TODO` came back as the string "# TODO" and a list item written as `- # none
|
|
26
|
+
yet` came back as "# none yet", so a downstream linter counted a comment placeholder as a
|
|
27
|
+
filled-in field and passed specs it should have failed. A comment-only value followed by a
|
|
28
|
+
more-indented block still opens that nested map or list, exactly as it did with no comment.
|
|
29
|
+
Quoted values are untouched: `key: "# literal"` still reads as "# literal".
|
|
30
|
+
|
|
3
31
|
## 0.2.0 (2026-09-28)
|
|
4
32
|
|
|
5
33
|
- The frontmatter reader reads nesting: maps inside maps, lists of maps, lists inside maps, and
|
package/SPEC.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The superskill standard
|
|
2
2
|
|
|
3
|
-
**Version 0.2.
|
|
3
|
+
**Version 0.2.2** (2026-09-29). The reference checker is `@supersuit/superskill`; where this
|
|
4
4
|
document and the checker disagree, the checker has a bug.
|
|
5
5
|
|
|
6
6
|
A **superskill** runs on frontier intelligence, is checked against examples a person approved,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/superskill",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Score any agent skill folder as skill, tested, or superskill. An open standard and a zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "superskill": "bin/superskill.mjs" },
|
package/src/frontmatter.mjs
CHANGED
|
@@ -27,6 +27,17 @@ const indentOf = (l) => l.length - l.trimStart().length;
|
|
|
27
27
|
// at any depth, read by indentation. Until then the reader stopped at one level and IGNORED
|
|
28
28
|
// anything deeper, so a list of maps came back as nothing and no error said so. Values still
|
|
29
29
|
// stay strings. A stray indented line at the top level is still tolerated, as before.
|
|
30
|
+
// 0.2.1: an unquoted value or list item that is only a comment (`key: # TODO`, `- # none yet`)
|
|
31
|
+
// reads as empty, the same as no value at all, which is what YAML means. A comment-only value
|
|
32
|
+
// that is followed by a more-indented block still opens that nested map or list, exactly as
|
|
33
|
+
// `key:` with nothing after it does.
|
|
34
|
+
// 0.2.2: an inline flow map (`scope: { form: essay, audience: builders }`) reads as an object,
|
|
35
|
+
// nested to any depth (a flow map inside a flow map, a flow list inside a flow map). And an
|
|
36
|
+
// inline flow list followed by a same-line comment (`conditions: [r1, r2, r3] # 5 to 10 ids`)
|
|
37
|
+
// reads as a list instead of the whole `[...] # ...` text, because the flow value is now parsed
|
|
38
|
+
// character-by-character (quote-aware) instead of by checking whether the raw value ends in `]`.
|
|
39
|
+
// Malformed flow syntax (unbalanced brackets) never throws: it falls back to the raw string, the
|
|
40
|
+
// same as an unrecognized value always has.
|
|
30
41
|
export function parseYamlSubset(lines) {
|
|
31
42
|
return parseMap(lines, 0, 0, true)[0];
|
|
32
43
|
}
|
|
@@ -62,7 +73,7 @@ function parseMap(lines, i, indent, top = false) {
|
|
|
62
73
|
const [v, next] = readBlock(lines, i, ind, rest[0]);
|
|
63
74
|
out[key] = v; i = next; continue;
|
|
64
75
|
}
|
|
65
|
-
if (rest === "") {
|
|
76
|
+
if (rest === "" || rest.startsWith("#")) {
|
|
66
77
|
const j = nextContent(lines, i);
|
|
67
78
|
if (j < lines.length) {
|
|
68
79
|
const ci = indentOf(lines[j]);
|
|
@@ -90,7 +101,7 @@ function parseList(lines, i, indent) {
|
|
|
90
101
|
const content = t === "-" ? "" : t.slice(1).trimStart();
|
|
91
102
|
const at = line.indexOf(content, ind + 1); // where the item's content starts on the line
|
|
92
103
|
i++;
|
|
93
|
-
if (content === "") {
|
|
104
|
+
if (content === "" || content.startsWith("#")) {
|
|
94
105
|
const j = nextContent(lines, i);
|
|
95
106
|
if (j < lines.length && indentOf(lines[j]) > ind) {
|
|
96
107
|
const ci = indentOf(lines[j]);
|
|
@@ -99,6 +110,16 @@ function parseList(lines, i, indent) {
|
|
|
99
110
|
} else out.push("");
|
|
100
111
|
continue;
|
|
101
112
|
}
|
|
113
|
+
if (content.startsWith("{") || content.startsWith("[")) {
|
|
114
|
+
// A bare flow map/list item (`- { station: fine, severity: fail }`, `- [a, b]`) is not a
|
|
115
|
+
// "- key: value" line, even though the KEY regex below would happily match on the first
|
|
116
|
+
// colon inside the braces (reading "{ station" as the key, which produced garbage). Try
|
|
117
|
+
// the flow parse first; when it is malformed, `inlineOrScalar` falls back to the raw
|
|
118
|
+
// string via `scalar()`, same as an unrecognized top-level value always has. Either way
|
|
119
|
+
// this never falls through to the "- key: value" submap heuristic below, because a flow
|
|
120
|
+
// collection's opening bracket can never legitimately be a map key.
|
|
121
|
+
out.push(inlineOrScalar(content)); continue;
|
|
122
|
+
}
|
|
102
123
|
if (KEY.test(content) && !/^\[.*\]$/.test(content)) {
|
|
103
124
|
// "- key: value" opens a map whose keys sit where this content starts.
|
|
104
125
|
const sub = [" ".repeat(at) + content, ...lines.slice(i)];
|
|
@@ -120,10 +141,88 @@ function readBlock(lines, i, keyIndent, style) {
|
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
function inlineOrScalar(rest) {
|
|
123
|
-
if (rest.startsWith("[")
|
|
144
|
+
if (rest.startsWith("[") || rest.startsWith("{")) {
|
|
145
|
+
const flow = tryParseFlow(rest);
|
|
146
|
+
if (flow !== undefined) return flow;
|
|
147
|
+
}
|
|
124
148
|
return scalar(rest);
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
// A flow collection (`[...]` or `{...}`) parsed character-by-character so quotes can protect a
|
|
152
|
+
// `,` `]` `}` or `#` from being read as structure, and so trailing whitespace plus a same-line
|
|
153
|
+
// comment after the closing bracket does not fall the whole value back to a raw string. Returns
|
|
154
|
+
// `undefined` (never throws) when `rest` is not a clean flow value: unbalanced brackets, an
|
|
155
|
+
// unterminated quote, or trailing content that is neither blank nor a comment. The caller falls
|
|
156
|
+
// back to `scalar(rest)` in every one of those cases, same as an unrecognized value always has.
|
|
157
|
+
function tryParseFlow(rest) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = readFlowCollection(rest, 0);
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const trailing = rest.slice(parsed.end);
|
|
165
|
+
if (trailing.trim() === "" || /^\s+#/.test(trailing)) return parsed.value;
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readFlowCollection(s, i) {
|
|
170
|
+
const open = s[i];
|
|
171
|
+
const close = open === "{" ? "}" : "]";
|
|
172
|
+
const isMap = open === "{";
|
|
173
|
+
i = skipFlowWs(s, i + 1);
|
|
174
|
+
if (s[i] === close) return { value: isMap ? {} : [], end: i + 1 };
|
|
175
|
+
const items = [];
|
|
176
|
+
for (;;) {
|
|
177
|
+
i = skipFlowWs(s, i);
|
|
178
|
+
if (i >= s.length) throw new Error("unterminated flow collection");
|
|
179
|
+
let key;
|
|
180
|
+
if (isMap) {
|
|
181
|
+
const k = readFlowToken(s, i, [":"]);
|
|
182
|
+
key = k.text.trim().replace(/^["']|["']$/g, "");
|
|
183
|
+
i = skipFlowWs(s, k.end + 1);
|
|
184
|
+
}
|
|
185
|
+
let value;
|
|
186
|
+
if (s[i] === "{" || s[i] === "[") {
|
|
187
|
+
const nested = readFlowCollection(s, i);
|
|
188
|
+
value = nested.value; i = nested.end;
|
|
189
|
+
} else {
|
|
190
|
+
const v = readFlowToken(s, i, [",", close]);
|
|
191
|
+
value = scalar(v.text);
|
|
192
|
+
i = v.end;
|
|
193
|
+
}
|
|
194
|
+
items.push(isMap ? [key, value] : value);
|
|
195
|
+
i = skipFlowWs(s, i);
|
|
196
|
+
if (s[i] === ",") { i = skipFlowWs(s, i + 1); if (s[i] === close) { i++; break; } continue; }
|
|
197
|
+
if (s[i] === close) { i++; break; }
|
|
198
|
+
throw new Error(`expected ',' or '${close}'`);
|
|
199
|
+
}
|
|
200
|
+
const value = isMap ? Object.fromEntries(items) : items.filter((v) => v !== "");
|
|
201
|
+
return { value, end: i };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function skipFlowWs(s, i) {
|
|
205
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
206
|
+
return i;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Reads raw text from `i` up to (but not including) the first unquoted occurrence of a char in
|
|
210
|
+
// `stopChars`, honoring both quote styles so a stop char inside quotes stays text. Throws (never
|
|
211
|
+
// returns a partial token) when the string runs out before a stop char is found outside quotes,
|
|
212
|
+
// which is what an unbalanced bracket or an unterminated quote looks like from here.
|
|
213
|
+
function readFlowToken(s, i, stopChars) {
|
|
214
|
+
let text = "";
|
|
215
|
+
let q = null;
|
|
216
|
+
while (i < s.length) {
|
|
217
|
+
const ch = s[i];
|
|
218
|
+
if (q) { text += ch; if (ch === q) q = null; i++; continue; }
|
|
219
|
+
if (ch === '"' || ch === "'") { q = ch; text += ch; i++; continue; }
|
|
220
|
+
if (stopChars.includes(ch)) return { text, end: i };
|
|
221
|
+
text += ch; i++;
|
|
222
|
+
}
|
|
223
|
+
throw new Error("unterminated flow token");
|
|
224
|
+
}
|
|
225
|
+
|
|
127
226
|
function foldLines(lines) {
|
|
128
227
|
let s = "";
|
|
129
228
|
for (const l of lines) {
|
|
@@ -133,19 +232,6 @@ function foldLines(lines) {
|
|
|
133
232
|
return s;
|
|
134
233
|
}
|
|
135
234
|
|
|
136
|
-
function splitInline(s) {
|
|
137
|
-
const parts = [];
|
|
138
|
-
let cur = "", q = null;
|
|
139
|
-
for (const ch of s) {
|
|
140
|
-
if (q) { cur += ch; if (ch === q) q = null; }
|
|
141
|
-
else if (ch === '"' || ch === "'") { q = ch; cur += ch; }
|
|
142
|
-
else if (ch === ",") { parts.push(cur); cur = ""; }
|
|
143
|
-
else cur += ch;
|
|
144
|
-
}
|
|
145
|
-
parts.push(cur);
|
|
146
|
-
return parts.map((p) => p.trim());
|
|
147
|
-
}
|
|
148
|
-
|
|
149
235
|
function scalar(raw) {
|
|
150
236
|
const s = String(raw).trim();
|
|
151
237
|
if (s.startsWith('"')) {
|