@gregiteen/ssss-cli 0.9.6 → 0.10.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 CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] - 2026-09-28
11
+ ### Added
12
+ - **`asset` primitive**: Text-encoded files (Base64 for small binaries, UTF-8 for code/SVGs) and pointer metadata for large files (`resource_ref`, `storage_uri`, `hash`).
13
+ - **`resource` primitive**: First-class core support for external cloud infrastructure, databases, S3 buckets, domains, and mailboxes, completing the `resource_bound` portability class (§5.5) with automatic `resource.binds` stripping on bundle export.
14
+ - **`trigger` primitive**: Standalone schedules and event sources (`cron`, `webhook`, `event`), decoupling procedural workflows from scheduling and enabling file-level distributed leasing without workflow lock contention.
15
+ - **`role` and `thread` primitives**: Canonical, concise forms of `security_role` and `conversation` with 100% backward-compatible alias resolution.
16
+ - **`docs/design.md` scaffolding**: `ssss new` now scaffolds a starter architecture and technical design specification alongside `README.md` and `CLAUDE.md`.
17
+ - **Project Management Audit integration**: `scaffold-project.mjs` now includes `AUDIT` in canonical `DOC_TYPES` (the 5-document SWE sequence).
18
+
19
+ ### Behavior changes hosts should check
20
+ - Core registry now recognizes `asset`, `resource`, `trigger`, `role`, and `thread`.
21
+ - Existing documents using `security_role` and `conversation` continue to validate without modification via alias resolution.
22
+
23
+ ## [0.9.7] - 2026-09-27
24
+ ### Fixed
25
+ - Frontmatter kept number-like strings as text only until the next patch. The
26
+ serializer wrote `"07003"` unquoted and the parser read any all-digit value
27
+ as an integer, so a zip code or license number with a leading zero lost it
28
+ (`07003` became `7003`) once any other field of the document was patched,
29
+ and YAML 1.1 readers such as PyYAML read the unquoted value as octal
30
+ (`07003` → 3587).
31
+
32
+ ### Behavior changes hosts should check
33
+ - Strings that would read back as a number, boolean or null (`"07003"`,
34
+ `"00123"`, `"1.10"`, `"true"`, `"null"`) are now written double-quoted.
35
+ - An unquoted scalar with a leading zero (`zip: 07003`) now parses as the
36
+ string `"07003"`, not an integer; plain integers (`0`, `42`, `-5`) are
37
+ unchanged. Documents already rewritten without the zero cannot be recovered
38
+ by the library.
39
+ - New conformance check: number-like strings survive repeated patches.
40
+
10
41
  ## [0.9.6] - 2026-09-25
11
42
  ### Changed
12
43
  - The push skill describes this repo's real release path. `/push` is always a
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.6
1
+ 0.10.0
@@ -106,7 +106,7 @@
106
106
  "file_count": 14,
107
107
  "provenance": {
108
108
  "content_hash": "sha256:7febd673938a3dc04c068e0bf08097a9783c09e612606d2df9a4db8dcb8907f5",
109
- "exporter": "@gregiteen/ssss-cli@0.9.6"
109
+ "exporter": "@gregiteen/ssss-cli@0.10.0"
110
110
  }
111
111
  },
112
112
  "files": [
@@ -22,6 +22,7 @@ my-app/
22
22
  test/ssss-conformance.test.mjs Replays the canonical fixtures through the engine
23
23
  AND round-trips the vault as a sale bundle.
24
24
  CLAUDE.md Agent guide (source-of-truth + portability rules).
25
+ docs/design.md Architecture & technical design specification.
25
26
  README.md .gitignore
26
27
  ```
27
28
 
package/docs/ssss-spec.md CHANGED
@@ -229,13 +229,18 @@ primitive — it implements the subset its product requires — but any primitiv
229
229
  |--------|--------|-------------|---------|
230
230
  | `memory` | knowledge | structural | A single unit of agent knowledge (rule, pattern, fact, preference). |
231
231
  | `skill` | capability | structural | A skill package manifest. |
232
+ | `asset` | capability | structural | A text-encoded code, vector, or binary asset payload or pointer. |
233
+ | `resource` | capability | resource_bound | An external infrastructure, cloud asset, domain, database, or API dependency. |
232
234
  | `rule` | governance | structural | A workspace-scoped behavior rule applied to assistants/agents. |
233
- | `security_role` | governance | structural | A security role definition for role-based access control (RBAC). |
235
+ | `role` | governance | structural | A security role definition for role-based access control (RBAC). |
236
+ | `security_role` | governance | structural | A security role definition for role-based access control (RBAC). Legacy alias of `role`. |
234
237
  | `task` | work | tenant_private | A unit of submitted work — one step or many. The universal work primitive. |
235
238
  | `assistant` | actor | structural | The definition of an AI assistant/persona. |
236
- | `workflow` | work | structural | A reusable **task template**: a defined procedure plus triggers. Firing a workflow submits a `task`. |
239
+ | `workflow` | work | structural | A reusable **task template**: a defined procedure. Firing a workflow submits a `task`. |
240
+ | `trigger` | work | structural | A standalone schedule or event-driven trigger that fires a workflow. |
237
241
  | `model` | catalog | structural | The definition of an inference model. |
238
- | `conversation` | transcript | tenant_private | An append-only chat transcript. |
242
+ | `thread` | transcript | tenant_private | An append-only chat transcript. |
243
+ | `conversation` | transcript | tenant_private | An append-only chat transcript. Legacy alias of `thread`. |
239
244
  | `run` | transcript | tenant_private | An append-only workflow execution record. |
240
245
  | `conflict` | meta | tenant_private | A record of two contradicting primitives, blocking promotion. |
241
246
  | `page` | capability | structural | A VFS-native sandboxed custom workspace page. |
@@ -344,6 +349,50 @@ timestamp: 2026-05-16T14:00:00Z
344
349
  ...
345
350
  ```
346
351
 
352
+ #### `asset`
353
+
354
+ A text-encoded asset payload: source code, vector graphics, or base64-encoded binary content, or pointer metadata for large files.
355
+
356
+ REQUIRED: `type`, `name`, `mime_type`, `encoding` (`base64|utf-8|none`).
357
+
358
+ ```markdown
359
+ ---
360
+ type: asset
361
+ name: "logo-icon"
362
+ title: "Brand Logo Icon"
363
+ description: "A 1x1 pixel PNG icon encoded in base64."
364
+ timestamp: 2026-05-16T14:00:00Z
365
+ mime_type: "image/png"
366
+ encoding: "base64"
367
+ ---
368
+
369
+ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
370
+ ```
371
+
372
+ #### `resource`
373
+
374
+ An external infrastructure, cloud asset, domain, database, or API dependency bound to the workspace. Carries `portability: resource_bound`.
375
+
376
+ REQUIRED: `type`, `name`, `kind`, `status` (`bound|unbound|pending|error`).
377
+
378
+ ```markdown
379
+ ---
380
+ type: resource
381
+ name: "Primary Database"
382
+ title: "Primary Database"
383
+ description: "Application production PostgreSQL database."
384
+ timestamp: 2026-05-16T14:00:00Z
385
+ kind: postgres
386
+ status: bound
387
+ binds:
388
+ host: "db.internal.net"
389
+ port: 5432
390
+ database: "app_prod"
391
+ ---
392
+
393
+ Production PostgreSQL database connection.
394
+ ```
395
+
347
396
  #### `task`
348
397
 
349
398
  A unit of submitted work — the universal work primitive. A `task` MAY be ad-hoc or
@@ -445,6 +494,28 @@ isActive: true
445
494
  3. Send digest.
446
495
  ```
447
496
 
497
+ #### `trigger`
498
+
499
+ A standalone schedule or event-driven trigger that fires a task from a workflow template.
500
+
501
+ REQUIRED: `type`, `name`, `source` (`cron|interval|webhook|event|file_change|condition|manual`), `status` (`active|paused|disabled`).
502
+
503
+ ```markdown
504
+ ---
505
+ type: trigger
506
+ name: "Daily Sync"
507
+ title: "Daily Sync Trigger"
508
+ description: "Fires the daily synchronization workflow at 08:00 UTC."
509
+ timestamp: 2026-05-16T14:00:00Z
510
+ source: cron
511
+ status: active
512
+ cron: "0 8 * * *"
513
+ target_workflow: "workflows/sync.md"
514
+ ---
515
+
516
+ Triggers daily synchronization.
517
+ ```
518
+
448
519
  #### `rule`
449
520
 
450
521
  A workspace-scoped behavior rule that constrains how assistants/agents act.
@@ -495,6 +566,27 @@ grants everything. A `security_role` is one way for a host to derive a
495
566
  principal's capabilities. The kernel only ever sees the resulting capability
496
567
  list, never the role name.
497
568
 
569
+ #### `role`
570
+
571
+ A named set of capabilities used for role-based access control (RBAC). Canonical form of `security_role`.
572
+
573
+ REQUIRED: `type`, `name`, `permissions` (array). OPTIONAL: `description`.
574
+
575
+ ```markdown
576
+ ---
577
+ type: role
578
+ name: "support-agent"
579
+ title: "Support agent"
580
+ description: "Can write assistants and events, nothing else."
581
+ timestamp: 2026-05-16T14:00:00Z
582
+ permissions:
583
+ - "ssss:assistant:*"
584
+ - "write:event"
585
+ ---
586
+
587
+ Maintains support assistants and records support events.
588
+ ```
589
+
498
590
  #### `model`
499
591
 
500
592
  The definition of an inference model.
@@ -543,6 +635,33 @@ Hello.
543
635
  Hi — how can I help?
544
636
  ```
545
637
 
638
+ #### `thread`
639
+
640
+ An append-only chat transcript. Append-type. Canonical form of `conversation`.
641
+
642
+ REQUIRED: `type`, `thread_id`. Typical fields: `workspace_id`, `user_id`, `status`,
643
+ `turn_count`, `created_at`.
644
+
645
+ ```markdown
646
+ ---
647
+ type: thread
648
+ title: "Support Thread"
649
+ description: "A two-turn customer support thread."
650
+ timestamp: 2026-05-16T14:00:05Z
651
+ thread_id: "7f3a2b1c-5d6e-4f70-8a91-b2c3d4e5f607"
652
+ workspace_id: "ws-acme"
653
+ user_id: "user-42"
654
+ status: active
655
+ created_at: 2026-05-16T14:00:00Z
656
+ ---
657
+
658
+ ### turn 1 — user — 2026-05-16T14:00:00Z
659
+ Hello.
660
+
661
+ ### turn 2 — assistant — 2026-05-16T14:00:05Z
662
+ Hi — how can I help?
663
+ ```
664
+
546
665
  #### `run`
547
666
 
548
667
  An append-only workflow execution record. Append-type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gregiteen/ssss-cli",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "CLI tools and the reference engine for the Structured Semantic Syntax System",
5
5
  "main": "src/engine.mjs",
6
6
  "exports": {
@@ -33,6 +33,36 @@
33
33
  "notes": "Compatible with the open Agent Skills standard (name + description). The `type: skill` discriminator is REQUIRED for SSSS-managed manifests.",
34
34
  "spec_ref": "§5.4 skill"
35
35
  },
36
+ "asset": {
37
+ "family": "capability",
38
+ "canonical_filename": null,
39
+ "filename_convention": "kebab-case slug matching the `slug` field",
40
+ "append_only": false,
41
+ "portability": "structural",
42
+ "required_fields": ["type", "name", "mime_type", "encoding"],
43
+ "optional_fields": ["slug", "hash", "size_bytes", "description"],
44
+ "enums": {
45
+ "encoding": ["base64", "utf-8", "none"]
46
+ },
47
+ "notes": "A text-encoded asset payload: source code, vector graphics, or base64-encoded binary content.",
48
+ "spec_ref": "§5.4 asset"
49
+ },
50
+ "resource": {
51
+ "family": "capability",
52
+ "canonical_filename": null,
53
+ "append_only": false,
54
+ "portability": "resource_bound",
55
+ "required_fields": ["type", "name", "kind", "status"],
56
+ "optional_fields": ["slug", "description", "binds", "provisioning"],
57
+ "enums": {
58
+ "status": ["bound", "unbound", "pending", "error"]
59
+ },
60
+ "resource": {
61
+ "binds": ["binds"]
62
+ },
63
+ "notes": "An external infrastructure, cloud asset, domain, database, or API dependency bound to the workspace.",
64
+ "spec_ref": "§5.4 resource"
65
+ },
36
66
  "rule": {
37
67
  "family": "governance",
38
68
  "canonical_filename": null,
@@ -42,6 +72,16 @@
42
72
  "optional_fields": ["description", "scope"],
43
73
  "spec_ref": "§5.4 rule"
44
74
  },
75
+ "role": {
76
+ "family": "governance",
77
+ "canonical_filename": "ROLE.md",
78
+ "append_only": false,
79
+ "portability": "structural",
80
+ "required_fields": ["type", "name", "permissions"],
81
+ "optional_fields": ["description"],
82
+ "notes": "A named set of capabilities used for role-based access control (RBAC). Canonical form of security_role.",
83
+ "spec_ref": "§5.4 role"
84
+ },
45
85
  "security_role": {
46
86
  "family": "governance",
47
87
  "canonical_filename": "ROLE.md",
@@ -78,6 +118,22 @@
78
118
  "optional_fields": ["workflow_id", "slug", "description", "triggers", "isActive", "priority", "misfire_policy", "concurrency"],
79
119
  "spec_ref": "§5.4 workflow"
80
120
  },
121
+ "trigger": {
122
+ "family": "work",
123
+ "canonical_filename": null,
124
+ "append_only": false,
125
+ "portability": "structural",
126
+ "required_fields": ["type", "name", "source", "status"],
127
+ "optional_fields": ["slug", "description", "cron", "interval", "timezone", "target_workflow", "target_params", "concurrency", "misfire_policy"],
128
+ "enums": {
129
+ "source": ["cron", "interval", "webhook", "event", "file_change", "condition", "manual"],
130
+ "status": ["active", "paused", "disabled"],
131
+ "concurrency": ["allow_parallel", "skip_if_running", "enqueue", "replace"],
132
+ "misfire_policy": ["skip", "run_once", "catch_up"]
133
+ },
134
+ "notes": "A standalone schedule or event-driven trigger that fires a task from a workflow template.",
135
+ "spec_ref": "§5.4 trigger"
136
+ },
81
137
  "model": {
82
138
  "family": "catalog",
83
139
  "canonical_filename": "MODEL.md",
@@ -86,6 +142,16 @@
86
142
  "required_fields": ["type", "model_id", "provider"],
87
143
  "spec_ref": "§5.4 model"
88
144
  },
145
+ "thread": {
146
+ "family": "transcript",
147
+ "canonical_filename": "CONVERSATION.md",
148
+ "append_only": true,
149
+ "portability": "tenant_private",
150
+ "required_fields": ["type", "thread_id"],
151
+ "optional_fields": ["workspace_id", "user_id", "status", "turn_count", "created_at"],
152
+ "notes": "An append-only multi-turn chat transcript. Canonical form of conversation.",
153
+ "spec_ref": "§5.4 thread"
154
+ },
89
155
  "conversation": {
90
156
  "family": "transcript",
91
157
  "canonical_filename": "CONVERSATION.md",
@@ -103,6 +103,7 @@ export async function run(argv) {
103
103
  W(root, '.gitignore', 'node_modules/\ndist/\n*.ucw.json\n.DS_Store\n');
104
104
  W(root, 'README.md', readme(name, withTR));
105
105
  W(root, 'CLAUDE.md', claudeMd(name, withTR));
106
+ W(root, 'docs/design.md', designMd(name));
106
107
 
107
108
  // ── git + install (best-effort, flag-gated, no shell) ───────────────────
108
109
  const log = (m) => console.error(m);
@@ -121,7 +122,9 @@ export async function run(argv) {
121
122
  }
122
123
  if (withTR) {
123
124
  log('\nInitializing Total Recall memory vault…');
124
- tryRun('npx', ['-p', 'total-recall-brain', 'total-recall', 'init'], root);
125
+ if (!tryRun('npx', ['-p', 'total-recall-brain', 'total-recall', 'init', '--project'], root)) {
126
+ die('scaffolded and installed, but project Total Recall init failed. Run `npx total-recall init --project` in the new directory.', 1);
127
+ }
125
128
  }
126
129
  log('\nVerifying the toolchain (npm test)…');
127
130
  if (!tryRun('npm', ['test'], root)) {
@@ -135,7 +138,7 @@ export async function run(argv) {
135
138
  console.log(` cd ${path.relative(process.cwd(), root) || '.'}`);
136
139
  if (!flags.install) {
137
140
  console.log(' npm install');
138
- if (withTR) console.log(' npx -p total-recall-brain total-recall init # seed the memory vault');
141
+ if (withTR) console.log(' npx -p total-recall-brain total-recall init --project # seed this project memory vault');
139
142
  }
140
143
  console.log(' npm test # replays the canonical fixtures + round-trips the vault');
141
144
  console.log(' npx ssss export vault --profile sale --out dist/bundle.ucw.json');
@@ -226,7 +229,7 @@ ${withTR ? `
226
229
  This project is also wired for [Total Recall](https://github.com/gregiteen/total-recall):
227
230
 
228
231
  \`\`\`bash
229
- npx -p total-recall-brain total-recall init # seed .agent/skills/total-recall
232
+ npx -p total-recall-brain total-recall init --project # seed this project memory vault
230
233
  npx total-recall remember fact "..." # persist a fact
231
234
  npx total-recall recall "..." # semantic recall
232
235
  \`\`\`
@@ -260,3 +263,29 @@ ${withTR ? `
260
263
  \`npx total-recall remember\` / \`recall\` CLI. Save corrections and decisions there.
261
264
  ` : ''}`;
262
265
  }
266
+
267
+ function designMd(name) {
268
+ return `# ${name} — System Architecture & Design
269
+
270
+ > Technical design and architecture specification for ${name}.
271
+
272
+ ## Overview
273
+
274
+ <High-level summary of the system and its primary goals.>
275
+
276
+ ## Component Architecture
277
+
278
+ - **Vault Structure**: Typed Markdown documents under \`vault/\` serving as the source of truth.
279
+ - **Workflows & Execution**: Automated procedures defined in \`vault/workflows/\` triggered by operational events.
280
+ - **Capabilities & Resources**: Tools and external service integrations required for execution.
281
+
282
+ ## Data & Schema Contracts
283
+
284
+ <Core entities, external dependencies, and integration interfaces.>
285
+
286
+ ## Open Decisions & Invariants
287
+
288
+ - SSSS VFS is the single source of truth for application state.
289
+ - Structural business logic is strictly decoupled from private tenant operational data.
290
+ `;
291
+ }
@@ -6,7 +6,7 @@ import { parseArgs, die, wantsHelp } from './lib/cli.mjs';
6
6
 
7
7
  const HELP = 'Usage: ssss registry compose|lock|verify [--extension <file> ...] [--lock <file>] [--out <file>]';
8
8
  const read = (file) => JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
9
- function serializable(set) { return { primitives: Object.fromEntries(set.primitives), aliases: Object.fromEntries(set.aliases), extension_versions: Object.fromEntries(set.extensionVersions) }; }
9
+ function serializable(set) { return { primitives: Object.fromEntries(set.types), aliases: Object.fromEntries(set.aliases), extension_versions: Object.fromEntries(set.extensionVersions) }; }
10
10
  export async function run(argv) {
11
11
  if (wantsHelp(argv)) { console.log(HELP); return; }
12
12
  const verb = argv[0];
@@ -404,6 +404,29 @@ async function runOperationContractRegressionConformance() {
404
404
  patchRes.success === true && Array.isArray(patchedData.triggers) && patchedData.triggers[0]?.cron === '0 8 * * *',
405
405
  patched);
406
406
 
407
+ const codeRes = await engine.processOperation({
408
+ type: 'patch',
409
+ idempotency_key: 'leading-zero-patch',
410
+ workspace_id: 'ws-reg',
411
+ path: workflowPath,
412
+ actor: { role: 'system' },
413
+ patches: { zip: '07003', license: '00123', flag: 'true' },
414
+ }, vault);
415
+ await engine.processOperation({
416
+ type: 'patch',
417
+ idempotency_key: 'leading-zero-repatch',
418
+ workspace_id: 'ws-reg',
419
+ path: workflowPath,
420
+ actor: { role: 'system' },
421
+ patches: { description: 'A later, unrelated patch.' },
422
+ }, vault);
423
+ const codeText = fs.readFileSync(path.join(vault, workflowPath), 'utf8');
424
+ const codeData = parseDocument(codeText).data;
425
+ check('number-like strings (zip 07003, license 00123, "true") survive repeated patches',
426
+ codeRes.success === true && codeData.zip === '07003' && codeData.license === '00123'
427
+ && codeData.flag === 'true' && codeText.includes('zip: "07003"'),
428
+ codeText);
429
+
407
430
  const leaseTarget = 'rules/leased.md';
408
431
  const leasePathKey = crypto.createHash('sha256').update(leaseTarget).digest('hex');
409
432
  const leaseDir = path.join(leaseStore, 'ws-reg');
@@ -728,6 +751,42 @@ async function runCliSmokeConformance() {
728
751
  typeof lockOut.integrity === 'string' && lockOut.integrity.startsWith('sha256:'),
729
752
  JSON.stringify(lockOut));
730
753
 
754
+ const composed = JSON.parse(execFileSync('node', [
755
+ path.join(__dirname, 'ssss.mjs'),
756
+ 'registry', 'compose',
757
+ '--extension', extensionPath,
758
+ ], { encoding: 'utf8' }));
759
+ check('ssss registry compose includes core and extension primitives',
760
+ !!composed.primitives.rule && composed.aliases['ssss:rule'] === 'rule' &&
761
+ composed.primitives[primitiveOut.primitive_id]?.registry === 'acme',
762
+ JSON.stringify(composed));
763
+
764
+ const fakeBin = path.join(tmp, 'bin');
765
+ const calls = path.join(tmp, 'install-calls.txt');
766
+ fs.mkdirSync(fakeBin);
767
+ for (const command of ['npm', 'npx']) {
768
+ const executable = path.join(fakeBin, command);
769
+ fs.writeFileSync(executable, '#!/bin/sh\nprintf "%s|%s|%s\\n" "$PWD" "$0" "$*" >> "$SSSS_TEST_CALLS"\n');
770
+ fs.chmodSync(executable, 0o755);
771
+ }
772
+ const trStarter = path.join(tmp, 'with-total-recall');
773
+ execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', trStarter,
774
+ '--with-total-recall', '--install', '--no-git'], {
775
+ stdio: 'ignore',
776
+ env: { ...process.env, PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`, SSSS_TEST_CALLS: calls },
777
+ });
778
+ const installCalls = fs.readFileSync(calls, 'utf8').trim().split('\n');
779
+ const normalizedCalls = installCalls.map((line) => {
780
+ const [cwd, executable, args] = line.split('|');
781
+ return `${fs.realpathSync(cwd)}|${path.basename(executable)}|${args}`;
782
+ });
783
+ const realStarter = fs.realpathSync(trStarter);
784
+ check('ssss new initializes only the new project Total Recall brain',
785
+ normalizedCalls.some((line) => line === `${realStarter}|npx|-p total-recall-brain total-recall init --project`) &&
786
+ normalizedCalls.some((line) => line === `${realStarter}|npm|install --no-audit --no-fund`) &&
787
+ normalizedCalls.some((line) => line === `${realStarter}|npm|test`),
788
+ JSON.stringify(installCalls));
789
+
731
790
  const adapterOut = JSON.parse(execFileSync('node', [
732
791
  path.join(__dirname, 'ssss.mjs'),
733
792
  'adapter', 'conformance',
@@ -36,7 +36,8 @@ function coerce(raw) {
36
36
  if (v === 'true') return true;
37
37
  if (v === 'false') return false;
38
38
  if (v === 'null' || v === '~') return null;
39
- if (/^-?\d+$/.test(v)) return parseInt(v, 10);
39
+ // A leading zero (zip 07003, license 00123) marks a code, not a number.
40
+ if (/^-?(0|[1-9]\d*)$/.test(v)) return parseInt(v, 10);
40
41
  if (/^-?\d*\.\d+$/.test(v)) return parseFloat(v);
41
42
  if (v.startsWith('[') && v.endsWith(']')) {
42
43
  const inner = v.slice(1, -1).trim();
@@ -200,12 +201,18 @@ export function parseDocument(content) {
200
201
  return { data: parseFrontmatter(fm), body };
201
202
  }
202
203
 
204
+ const LOOKS_TYPED =
205
+ /^(?:[-+]?\d+(?:\.\d*)?(?:[eE][-+]?\d+)?|[-+]?\.\d+|0[xXoObB][0-9a-fA-F_]+|true|false|yes|no|on|off|null|~|\.inf|\.nan)$/i;
206
+
203
207
  function serializeValue(v) {
204
208
  if (v === null) return 'null';
205
209
  if (typeof v === 'boolean' || typeof v === 'number') return String(v);
206
210
  if (Array.isArray(v)) return `[${v.map((x) => serializeValue(x)).join(', ')}]`;
207
211
  const s = String(v);
208
212
  if (s === '' || /[:#\[\]{}"'\\\n\r\t|>]/.test(s) || /^\s|\s$/.test(s)) return JSON.stringify(s);
213
+ // A string that would read back as a number, boolean or null stays quoted,
214
+ // so "07003" and "true" survive a round trip (here and in YAML 1.1 readers).
215
+ if (LOOKS_TYPED.test(s)) return JSON.stringify(s);
209
216
  return s;
210
217
  }
211
218