@gregiteen/ssss-cli 0.11.1 → 0.11.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 +7 -0
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +1 -1
- package/package.json +1 -1
- package/scripts/check-frontmatter.mjs +10 -0
- package/src/frontmatter.mjs +33 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.2] - 2026-10-19
|
|
11
|
+
### Added
|
|
12
|
+
- `parseFrontmatter(fm, { strict: true })` throws a `SyntaxError` on lines that
|
|
13
|
+
are not `key: value`, orphaned indentation, and malformed quoted or inline
|
|
14
|
+
values, so hosts such as Total Recall can report unreadable files. The default
|
|
15
|
+
stays lenient. Strict mode accepts all 30,518 files in 57 Total Recall vaults.
|
|
16
|
+
|
|
10
17
|
## [0.11.1] - 2026-10-19
|
|
11
18
|
### Fixed
|
|
12
19
|
- The frontmatter parser (`@gregiteen/ssss-cli/frontmatter`) now reads the YAML
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.11.
|
|
1
|
+
0.11.2
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"file_count": 14,
|
|
107
107
|
"provenance": {
|
|
108
108
|
"content_hash": "sha256:7febd673938a3dc04c068e0bf08097a9783c09e612606d2df9a4db8dcb8907f5",
|
|
109
|
-
"exporter": "@gregiteen/ssss-cli@0.11.
|
|
109
|
+
"exporter": "@gregiteen/ssss-cli@0.11.2"
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"files": [
|
package/package.json
CHANGED
|
@@ -89,6 +89,16 @@ check('single-line files with literal \\n are repaired on read', () => {
|
|
|
89
89
|
assert.equal(parseDocument('---\nk: a\\nb\n---\n').data.k, 'a\\nb');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
check('strict mode throws on malformed YAML that lenient mode skips', () => {
|
|
93
|
+
const bad = ': : :\nnot: [valid\n';
|
|
94
|
+
assert.doesNotThrow(() => parseFrontmatter(bad));
|
|
95
|
+
assert.throws(() => parseFrontmatter(bad, { strict: true }), SyntaxError);
|
|
96
|
+
assert.throws(() => parseFrontmatter('k: "open\nj: 1\n', { strict: true }), SyntaxError);
|
|
97
|
+
assert.throws(() => parseFrontmatter('a: 1\n b: 2\n', { strict: true }), SyntaxError);
|
|
98
|
+
assert.deepEqual(parseFrontmatter('a: [1, 2]\nb: {c: d}\n', { strict: true }), { a: [1, 2], b: { c: 'd' } });
|
|
99
|
+
assert.doesNotThrow(() => parseFrontmatter(bad)); // strict does not leak into later calls
|
|
100
|
+
});
|
|
101
|
+
|
|
92
102
|
check('serialize → parse round-trips tricky values', () => {
|
|
93
103
|
const data = {
|
|
94
104
|
'allowed-tools': ['Read', 'Bash'],
|
package/src/frontmatter.mjs
CHANGED
|
@@ -170,16 +170,24 @@ function parseFlow(src) {
|
|
|
170
170
|
return value;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Set by parseFrontmatter(fm, { strict: true }): malformed input throws instead of
|
|
174
|
+
// being skipped or kept as raw text.
|
|
175
|
+
let strictMode = false;
|
|
176
|
+
|
|
173
177
|
function coerce(raw) {
|
|
174
178
|
const v = raw.trim();
|
|
175
179
|
if (v === '') return '';
|
|
176
180
|
if (v[0] === '"' || v[0] === "'") {
|
|
177
181
|
const end = findClosingQuote(v);
|
|
178
182
|
if (end > 0 && /^\s*(#.*)?$/.test(v.slice(end + 1))) return unquote(v.slice(0, end + 1));
|
|
183
|
+
if (strictMode) throw new SyntaxError(`Malformed quoted value: ${v.slice(0, 60)}`);
|
|
179
184
|
return v;
|
|
180
185
|
}
|
|
181
186
|
if (v[0] === '[' || v[0] === '{') {
|
|
182
|
-
try { return parseFlow(v); } catch {
|
|
187
|
+
try { return parseFlow(v); } catch (err) {
|
|
188
|
+
if (strictMode) throw new SyntaxError(`Malformed inline value: ${v.slice(0, 60)}`);
|
|
189
|
+
return v;
|
|
190
|
+
}
|
|
183
191
|
}
|
|
184
192
|
return coercePlain(stripComment(v));
|
|
185
193
|
}
|
|
@@ -214,6 +222,7 @@ function readScalar(lines, i, keyIndent, rest) {
|
|
|
214
222
|
if (!line.trim()) { blanks++; j++; continue; }
|
|
215
223
|
if (countIndent(line) <= keyIndent) break;
|
|
216
224
|
if (!inline && line.trim().startsWith('#')) break;
|
|
225
|
+
if (strictMode && !inline && splitKeyValue(line.trim())) throw new SyntaxError(`Unexpected key in a wrapped value: ${line.trim().slice(0, 60)}`);
|
|
217
226
|
const piece = inline ? line.trim() : stripComment(line.trim());
|
|
218
227
|
text += blanks ? '\n'.repeat(blanks) + piece : ` ${piece}`;
|
|
219
228
|
blanks = 0;
|
|
@@ -278,9 +287,17 @@ function parseMap(lines, start, indent) {
|
|
|
278
287
|
if (!line.trim() || line.trim().startsWith('#')) { i++; continue; }
|
|
279
288
|
const currentIndent = countIndent(line);
|
|
280
289
|
if (currentIndent < indent) break;
|
|
281
|
-
if (currentIndent > indent) {
|
|
290
|
+
if (currentIndent > indent) {
|
|
291
|
+
if (strictMode) throw new SyntaxError(`Unexpected indentation: ${line.trim().slice(0, 60)}`);
|
|
292
|
+
i++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
282
295
|
const kv = splitKeyValue(line.slice(indent));
|
|
283
|
-
if (!kv) {
|
|
296
|
+
if (!kv) {
|
|
297
|
+
if (strictMode) throw new SyntaxError(`Not a "key: value" line: ${line.trim().slice(0, 60)}`);
|
|
298
|
+
i++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
284
301
|
const block = kv.rest.match(BLOCK_SCALAR_RE);
|
|
285
302
|
if (block) {
|
|
286
303
|
const parsed = parseBlockScalar(lines, i + 1, indent, block[1], block[2]);
|
|
@@ -369,12 +386,21 @@ function parseList(lines, start, indent) {
|
|
|
369
386
|
}
|
|
370
387
|
|
|
371
388
|
/**
|
|
372
|
-
* Parse a frontmatter block (the inner YAML, no fences) into an object.
|
|
389
|
+
* Parse a frontmatter block (the inner YAML, no fences) into an object. The
|
|
390
|
+
* default is lenient: unreadable lines are skipped. `{ strict: true }` throws a
|
|
391
|
+
* SyntaxError on lines that are not `key: value`, orphaned indentation, and
|
|
392
|
+
* malformed quoted or inline values, so callers can report unreadable files.
|
|
373
393
|
*/
|
|
374
|
-
export function parseFrontmatter(fm) {
|
|
394
|
+
export function parseFrontmatter(fm, { strict = false } = {}) {
|
|
375
395
|
if (!fm) return {};
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
const previous = strictMode;
|
|
397
|
+
strictMode = strict;
|
|
398
|
+
try {
|
|
399
|
+
// Drop the newline before the closing fence so it is not read as a blank line.
|
|
400
|
+
return parseMap(fm.replace(/\n$/, '').split('\n'), 0, 0).value;
|
|
401
|
+
} finally {
|
|
402
|
+
strictMode = previous;
|
|
403
|
+
}
|
|
378
404
|
}
|
|
379
405
|
|
|
380
406
|
/** Parse a full document (with fences) into { data, body }. */
|