Code View

fson / source / fson-1.1.0.0 / CHANGELOG.md
Preview
# Changelog

All notable changes to the fson project. Versions follow the project's
4-part scheme `X.Y.Z.W` (Y = topic).

## 1.1.0.0 — two fixes found before publishing

A site-content review of the install/Getting Started pages, done against
this release before it ever left the workspace (not pushed, not deployed,
not downloaded), turned up two real issues — folded in here rather than
shipped as a same-day 1.1.0.1/1.1.0.2 patch:

- **Release archive: guard `add_subdirectory(tests)`.** `ws-release.sh`'s
  smoke test (extract the public archive → `conan install` → configure)
  failed: `release.conf` excludes `tests/` from the download by design,
  but `CMakeLists.txt` did `if(${TESTING}) add_subdirectory(tests)`
  unconditionally, and `TESTING` defaults to `FSON_STANDALONE` (on) —
  exactly the case for the install page's own `conan create
  ~/path/to/fson` against the archive with default options. Same fix as
  the `docs/` guard in v1.0.1.0: `if(${TESTING} AND EXISTS
  "${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt")`.
- **Getting Started: two round-trip gaps made explicit.**
  `docs/getting_started.md` held two true-but-unstated behaviours: §1
  Loading a file — a missing file adds *no* entry to `result.errors`, so
  `result.document == nullptr` / `!ok()` is the only signal for "could
  not open"; §4 Saving — "writes back everything" did not say layout is
  excluded — arrays are always re-wrapped one element per line, and the
  writer inserts whatever commas that needs.

No API change either way.

## 1.0.2.0 — `tools/release.conf` for ws-release.sh

`workbench/bin/ws-release.sh` cuts a showcase-site release (Doxygen tree +
`git archive` tarballs + SHA-256) and stages it into the `fson.fedem.eu`
tenant. `tools/release.conf` records the Doxygen HTML path and notes that
the tenant download page's "Editor tooling" section (the `ffs-lang-tools`
`.vsix` / highlight.js `.tgz` packages) stays hand-maintained — outside
the `<!-- ws-release:table -->` markers ws-release.sh regenerates.

`.gitignore` += `dist/` (the `*.tar.gz` / `*.zip` globs already covered
the archives but not the `.sha256` sidecars ws-release.sh writes beside
them). No source / API change.

## 1.0.1.0 — `conan create` fix: guard `add_subdirectory(docs)`

`conan create fson` (default options, `with_docs=False`) failed at
configure:

```
CMake Error at CMakeLists.txt:77 (add_subdirectory):
  add_subdirectory given source "docs" which is not an existing directory.
```

`FSON_STANDALONE` is true in the Conan build (fson is the top-level
project there), so `add_subdirectory(docs)` ran — but the recipe's
`exports_sources` never shipped `docs/`, so the directory was absent.

- `CMakeLists.txt` — `if(FSON_STANDALONE)` → `if(FSON_STANDALONE AND
  EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/docs/CMakeLists.txt")`, matching
  the `callonce` / `plugin` recipes.
- `conanfile.py` — `exports_sources` now includes `docs/CMakeLists.txt`
  and `docs/Doxyfile.in` (just the two files the Doxygen target needs —
  it scans `libs/` + `apps/`, not the `docs/` markdown or the railroad
  sources), so `-o with_docs=True` builds again.

No source / API change. `conan create` verified green for both
`with_docs=False` and `with_docs=True`.

## 1.0.0.0 — first stable release

The API has been stable since the writer dialects and `%include` landed
(topics 8–11); `1.0.0` marks it as such and cuts the first published
source release. Source archives (`git archive` snapshots at the tag,
`.tar.gz` / `.zip` + `.sha256`) are on <https://fson.fedem.eu/downloads>.

No API change from `0.12.3.0` — this is a version + `SOVERSION` bump.

- `project(fson VERSION 1.0.0.0)`; `SOVERSION 0` → `1`
  (`libs/internal/sdk/fson/CMakeLists.txt`). The soname is now
  `libfson.so.1`.
- `.gitattributes` — `export-ignore` for `.gitattributes` and
  `.claude-package/` so the release tarballs are clean.
- The project was relicensed to **MIT** in `0.12.3.0` (see below); the
  first release ships under MIT.

Post-1.0, breaking changes stay permitted with a major bump — the project
has no compatibility guarantee across major versions (see `CLAUDE.md` §8).

Known, not release-blocking: the root `CMakeLists.txt` CPack section
carries dead `CPACK_*_CPARSE_*` variable names (a copy-paste from cparse;
the component is `fson`), and `cmake/packaging.cmake` is not `include()`d.
`cpack` DEB/RPM output is therefore not wired up — the `git archive`
tarballs are the release. Tracked as a follow-up.

## 0.12.3.0 — MIT licence

`fson` is now **MIT-licensed** (was Proprietary), matching its sibling
libraries `cparse` and `callonce` and its own dependency `cparse`. A
config-parsing library has no direct commercial value; the family's value
is adoption + engineering credibility, which a permissive licence serves
and a proprietary one blocks (corporate OSS review, distro packaging,
Conan-center). Relicensed now while there is one copyright holder and no
release.

- `LICENSE` — full MIT text (`Copyright (c) Fedem (Fehmi Demiralp)`).
- `CMakeLists.txt` / `cmake/packaging.cmake` — `CPACK_*_LICENSE`
  `"Proprietary"` → `"MIT"`. `conanfile.py` `license = "MIT"`.
  `README.md`, `CLAUDE.md` updated.
- `TRADEMARKS.md` (new) — the code is MIT; the names **FSON** / **FEDEM**
  and the branding are not. Fork = rename.
- `CONTRIBUTING.md` (new) — inbound = outbound (MIT), no CLA.
- `// SPDX-License-Identifier: MIT` on every source file
  (`libs/internal/sdk/fson/`, `apps/fson-check/`, the completion scripts).

## 0.12.2.0 — Doxygen `docs` target + `/api` chain

- `docs/CMakeLists.txt` + `docs/Doxyfile.in`: a `docs` target (Doxygen,
  optional Graphviz `dot`) → `build/*/docs/html/`, published at
  <https://fson.fedem.eu/api>. Standalone-only.
- `conanfile.py` `with_docs` option runs it during `conan build`.
- `docs/notation/svg2png.sh` rewritten: rasterises the railroad `.svg`
  sources to `.png` for the web, splicing in a cparse-matched blue-grey
  palette (`docs/notation/railroad-style.css`).

## 0.12.1.0 — `--` disables a whole multi-line `|`-block
- `--` now correctly disables an entire multi-line `|`-block, including a
  **keyless** one. `--|line one` / `|line two` parses the whole contiguous
  `|`-block, stores it as a single disabled, keyless member (invisible to
  queries, preserved structurally), and resumes parsing at the next
  member. Previously `--|…` was a parse error and the block's later lines
  were mis-read.
- This is the one place a value may appear without a key, and only when
  disabled. It round-trips as `--|…` lines (no key, no `:`); idempotent.
- (Disabling a multi-line array/object value, e.g. `--dizi: [ … ]`
  spanning lines, already worked — the whole value is parsed and skipped;
  a regression test now guards it.)
- Docs: `docs/notation.md` "Disabled members" section documents that `--`
  disables the whole following definition, keyless `|`-blocks included.

## 0.12.0.0 — multi-line `|`-block strings
- A string value may be written as a block of `|`-lines instead of a
  quoted literal (`00040-string.rrd`, third form). Each source line begins
  with `|`; the `|` is not part of the value; the content runs to end of
  line; a `\n` is appended for every line, the last included
  (`|abc`/`|def` → `"abc\ndef\n"`). A `\` right before the line break is a
  continuation (that newline is dropped, the next `|`-line joins). Escapes
  are decoded as in quoted strings.
- `|`-lines must be contiguous (only whitespace, or a `/* */` closing on
  the same line, may precede `|`); a blank line, `//` comment, or other
  content ends the block. The separating comma is optional after a block
  (a comma on the block line would be read as content), in both objects
  and arrays.
- `String` gains `enum class Form { Quoted, Multiline }` with
  `getForm()`/`setForm()`; the parser tags `|`-values `Multiline`. The
  writer re-emits the block form only in the Fson dialect and only when
  the content ends in `\n`, otherwise (and under Json/Json5) it writes a
  quoted string. Presentation is normalised; content is preserved exactly,
  and round-trip is idempotent (no comma accretion).
- Grammar `00040-string.rrd`/`.svg` updated with the block alternative.

## 0.11.1.0 — test completeness and roadmap closure
- Filled remaining GTest gaps in the public API surface: the entry-order
  machinery (`appendInclude`/`eraseInclude`/`eraseMember` index shifting,
  `getEntry`/`EntryKind`, and `Object::clone` copying includes + entry
  order), `Include` node accessors (path/alias/disabled/clone), `Member`
  provenance accessors (`setOrigin`/`getOriginFile`/`getOriginLocalPath`/
  `isMerged`, including the host-only origin-clear), and directive
  rendering across dialects (a `%include` is omitted under `Json`/`Json5`).
  Test totals: 130 fson cases across the five suites; ctest 9/9 green.
- Roadmap: the deferred "included-writable `setPath`" item is **closed
  (decided against)** — the host-only write policy plus the provenance API
  already cover locating and editing a value's source file, so no separate
  write-to-included-file feature will be added.
- No library code changed in this release (tests and docs only).

## 0.11.0.0 — reload staleness probe
- `Fson::snapshot( filename ) → Snapshot` records mtime, size and an
  FNV-1a 64-bit content hash of every file needed to resolve `filename`
  (host + includes). Empty when the host is missing; never throws.
- `Fson::isStale( filename, snapshot, mode )` reports whether any covered
  file changed since the snapshot. `StaleMode::MtimeSize` (default) is a
  cheap stat-only compare (a bare touch counts as stale);
  `StaleMode::ContentHash` re-reads and compares the hash (a touch with
  identical bytes is not stale). One snapshot serves either mode. Also
  detects a deleted dependency and a changed include graph (a file added
  or removed), and treats a previously-missing host that now exists as
  stale. Never throws.
- No external dependency is added: the hash is a small inline FNV-1a
  (non-cryptographic, sufficient for change detection). fson still runs no
  watcher thread — the application drives the probe on its own schedule or
  OS file events. This completes Topic 11 (reload support): the file set
  shipped in 0.10.1.0, the change probe ships here.

## 0.10.1.0 — complete dependency file set
- `Fson::LoadResult` gains `dependencySet`: the full set of files needed
  to resolve the document — the host file **plus** every included file,
  de-duplicated and sorted. Unlike `dependencies` (includes only, host
  excluded, kept for compatibility), this is the ready-made watch list for
  reload — no manual host addition.
- New `Fson::dependenciesOf( filename ) → std::set<std::string>` returns
  that same set WITHOUT keeping the resolved document, for reload/watch
  callers that only need the file list. A missing host file yields an
  empty set; parse/resolution errors never throw.
- Groundwork for Topic 11 (reload): a caller can now obtain, in one call,
  exactly the set of paths whose modification could change the resolved
  document.

## 0.10.0.0 — %include directives
- New `%include "path"` object-body directive merges the target file's
  root-object members into the including object as **defaults**: a key
  already present in the host wins (host over base), and the merge is
  **shallow** (a host key wholly shadows the base's, with no deep merge
  of nested objects). `%include "path" as name` instead binds the
  target's whole root value under `name` (the target root need not be an
  object). A directive may be disabled with `--%include`, which preserves
  it structurally but skips resolution.
- The directive character is `%` (not `@`, reserved for FFS `@placeholder@`;
  not `#`, deliberately left unused). See `docs/notation.md`.
- Directives are first-class structural nodes (`fedem::fson::Include`),
  kept in source order alongside members (`Object::EntryKind`/`getEntry`)
  and round-tripped verbatim in the Fson dialect. Merged-in members are
  **query-view only** — they are never written back into the host file.
- `Fson::load()` resolves directives after parsing: relative targets are
  anchored to the including file's directory (not the process CWD),
  include cycles are reported and broken (no exception, per the
  collect-and-continue contract), and every opened file is exposed via
  `LoadResult::dependencies` (for reload use — see the Topic 11 note).
- Write policy is **host-only**: `setPath()` on a merged value promotes it
  to a host member (its `%include` source file is never modified). A new
  provenance API reports where a resolved value comes from:
  `isLocal(path)`, `provenanceOf(path)` (source file + local path within
  it, differing from the query path for aliases), and `provenanceMap()`.
- Migration: none required. Documents without `%include` are unaffected;
  the new `Object` include/entry/provenance methods and the `Include`
  class are additive.

## 0.9.2.0 — add -Winit-self to the fson compile flag set
- The `fson` library target now builds with `-Winit-self` (GNU/Clang,
  `PRIVATE`). It diagnoses self-referential initialisation, which is not
  implied by `-Wall`/`-Wextra` and so had no coverage in the flag set.
  Not propagated to consumers or the cparse submodule.

## 0.9.1.0 — quiet load of missing files
- `Fson::load()` now checks `std::filesystem::exists()` before invoking
  the parser. A missing file returns the same not-found result as before
  (`document == nullptr`, so `ok() == false`) but WITHOUT the cparse
  layer writing "[Parser::parse] filesystem error: cannot make canonical
  path" to `std::clog`. Optional-config callers that probe for a file and
  fall back to defaults no longer emit that line on every load. A file
  that exists but is unreadable/unparseable still goes through the parser
  and surfaces its real diagnostics unchanged.

## 0.9.0.0 — JSON5-superset grammar
- Document root may be any value, not just an object (e.g. a bare array,
  string, number, boolean, or `null` is now a valid FSON file).
- Strings may be single- or double-quoted; new escapes `\xXX`, `\v`,
  `\0`, and a line continuation (`\` + line terminator); any other
  character after `\` is itself, with no error (a deliberate
  simplification vs. strict JSON5's narrower escape grammar). Raw
  U+2028/U+2029 are allowed unescaped inside strings.
- A trailing comma is now accepted in object and array bodies.
- Numbers accept a leading `+`, `0x`/`0X` hex literals, a leading or
  trailing `.`, and the literal tokens `Infinity`/`-Infinity`/`NaN`; new
  `Number::Kind` records which form a number came from.
  `Number::setValue(double)` now accepts non-finite values (previously
  threw `std::invalid_argument`).
- Whitespace additionally recognises NBSP, the BOM, U+2028/2029, and the
  rest of Unicode category Zs.
- New `FsonWriter::Dialect::Json5`: JSON5-compliant output that, unlike
  `Json`, keeps comments and bare keys and writes number lexemes
  verbatim (hex stays hex, `Infinity`/`-Infinity`/`NaN` stay literal).
  New `FsonWriter::DisabledMemberPolicy` (`ConvertToComment` default,
  `Keep`, `Drop`) replaces the boolean keep-disabled flag for the
  `Json`/`Json5` dialects (the old `setKeepDisabledMembers`/
  `getKeepDisabledMembers` remain as compatibility shims). The `Json`
  dialect now rewrites hex lexemes to plain decimal and throws
  `std::domain_error` when asked to write `Infinity`/`-Infinity`/`NaN`.
- `Document::getRootObject() → Object*` added (`nullptr` on a non-object
  root); `Document::getRoot() → Object&` now throws `std::out_of_range`
  on a non-object root instead of assuming one.
- `fson-check`: new `--json5` and `--drop-disabled` flags; `--keep`/
  `--drop-disabled` are mutually exclusive and each now require `--json`
  *or* `--json5`; `--bare` is compatible with `--json5` (still excluded
  with `--json`); a value that cannot be represented in the chosen
  dialect is now reported as a write failure instead of crashing.
- Nested block comments remain supported — the one deliberate exception
  to FSON's JSON5-superset claim, since JSON5 itself forbids nesting;
  FSON's bare-key grammar also remains narrower than JSON5's full
  `IdentifierName` production. See `docs/notation.md`, "FSON and JSON5".

  **Migration risk:** callers of `Document::getRoot()` may now see a
  runtime `std::out_of_range` thrown where they previously got an
  `Object&` unconditionally — this happens only when the document's
  root is not an object (impossible before this release, and still the
  default for a freshly-constructed `Document` or any document whose
  source begins with `{`). Existing code that always parses or
  constructs object-rooted documents is unaffected; code that might
  encounter a non-object root should switch to `getRootObject()` or
  catch the exception. No other breaking changes are introduced by this
  release.

## 0.8.0.0 — JSON conversion and key normalization
- `FsonWriter` dialects: `Json` (comments removed, all keys quoted,
  disabled members dropped or — with keep-disabled — emitted without the
  `--` prefix) alongside the lossless default `Fson` dialect; new
  prefer-bare-keys normalization for the Fson dialect.
- `fson-check --json [--keep]` and `--bare` options with combination
  checks (`--json` excludes `--bare` and `-i`; `--keep` requires
  `--json`).

## 0.7.0.0 — apps, examples, documentation
- `fson-check` command-line validator/formatter (stdout, `-i` in-place and
  `--output` modes; diagnostics on stderr).
- Annotated example FSON files under `examples/`.
- Documentation set: notation guide, getting-started tutorial, reference
  manual, integration guide, README, this changelog.

## 0.6.0.0 — packaging
- `fson.pc` fixed (`-lfson -lcparse`); install/CPack moved to
  `cmake/packaging.cmake`; component-aware 7Z/TGZ/ZIP/DEB/RPM packages
  (Runtime + DEVELOPMENT); library version follows the project version.

## 0.5.0.0 — query/modify API and façade
- Dotted-path access incl. `[N]` array segments; typed getters; `setPath`
  with intermediate-object creation and failure rollback; `removePath`.
- `Fson::load`/`Fson::save` façade returning errors instead of throwing.

## 0.4.0.0 — writer
- Comment-preserving pretty-printer (2-space indent, compact empty
  containers, adjacent `--key`, mandatory-only string escaping, trailing
  newline). Round-trip and fixed-point invariants tested.

## 0.3.0.0 — parser
- `FsonParser` on the cparse core: full grammar, nested block comments,
  comment attachment, `\uXXXX` → UTF-8, collect-and-continue error
  recovery, duplicate-key detection.

## 0.2.0.0 — model
- Grammar fixes (`null` literal, `\r` escape, `--` for quoted keys, root
  diagram) and the `fedem::fson` document model with lossless trivia.

## 0.1.x.0 — project setup
- Repository skeleton, FSON railroad-diagram notation, roadmap.

## Unreleased — end of input inside an open scope is no longer a clean ending

`Parser::parse` could not tell "the input ended because the grammar finished"
from "the input ended in the middle of something". Both arrive as the same
`ios_base::failure` from the cursor, and the driver treated both as a normal
EOF: `result = true`, no diagnostics.

Measured shapes before the fix:

| input | result |
|---|---|
| `{ a: 1` | `ok=false` — "unterminated object" |
| `{ a: 1, ` | **`ok=true`, errors=0** |
| `{` | **`ok=true`, errors=0** |
| `{ users: [ { username: "x", ` | **`ok=true`, errors=0** |

The difference is which read path is taken. Without a comma,
`extractCharacter` falls through to the non-throwing `safePeek`, sees
`END_OF_FILE` and `parseObjectBody` records its error properly. After a
comma the cursor read throws, and the exception unwinds past that check
straight to the driver, which calls it success.

For a configuration format this is severe: a file cut short at a member
boundary — an interrupted copy, a full disk, a killed editor — loads as valid
with the missing half silently falling back to defaults. Config files are
written with `,\n` separators, so the truncation point is very likely to land
exactly on the shape that was accepted.

**Fix, in the driver rather than in one grammar.** `cparse::Parser` gains an
opt-in open-scope counter: `enterScope()`, `leaveScope()`, `openScopeDepth()`.
When end-of-input unwinds with a depth above zero, the driver records a Syntax
error and fails the parse instead of reporting success.

Purely additive for existing cparse users: a grammar that never calls these
keeps a depth of zero and behaves exactly as before.

Deliberately NOT an RAII guard. The depth must still be non-zero while the
end-of-input exception unwinds — that is the condition being detected. A guard
decrementing during unwinding would erase the evidence. `leaveScope()` is
called only where a closer was actually consumed.

Only the outermost `parse()` resets the counter. A nested `parse()` is an
`%include`, and the including grammar legitimately has scopes open around it;
clearing the count there would hide a truncated include behind its parent's
braces.

`FsonParser` declares its scopes at the two points that consume `{` and `[`.

Existing diagnostics are unchanged: `{ a: 1 ` still reports
"unterminated object: expected '}'" through the old path, so
`FsonParserTests.unterminatedObjectIsReported` keeps its expected text.