@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 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
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.1"
109
+ "exporter": "@gregiteen/ssss-cli@0.11.2"
110
110
  }
111
111
  },
112
112
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gregiteen/ssss-cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "CLI tools and the reference engine for the Structured Semantic Syntax System",
5
5
  "main": "src/engine.mjs",
6
6
  "exports": {
@@ -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'],
@@ -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 { return v; }
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) { i++; continue; }
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) { i++; continue; }
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
- // Drop the newline before the closing fence so it is not read as a blank line.
377
- return parseMap(fm.replace(/\n$/, '').split('\n'), 0, 0).value;
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 }. */