# 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.