6. Adoption and CI¶
English | Português (Brasil)
← Tools · Index · Conformance and readers →
This chapter is the map. The full guide, with every edge case, is ADOPTING.md.
The four-step path¶
flowchart LR
A["1. vendor update<br/>brings in the tag"] --> B["2. git add vendor/<br/>+ gate --update"]
B --> C["3. commit<br/>vendor + baseline"]
C --> D["4. CI: check + gate<br/>on every PR"]
Run this at the root of your repository, with Python 3.12 or later (the floor since 4.35.0; CI runs 3.12, 3.14 and 3.15, and the optional
vector extra stops at 3.14 until onnxruntime ships for 3.15):
# 1. bring in the release, by tag, from the public repository
git clone https://github.com/marciocar/kg-ssot ../kg-ssot
python3 -I -B ../kg-ssot/tools/kg_vendor.py update --tag kg-ssot-v4.45.0 --dest vendor/kg-ssot
# 2. the dependencies in a venv, then the baseline (the gate only measures tracked files: git add first)
python3 -m venv .venv && .venv/bin/pip install --require-hashes -r vendor/kg-ssot/tools/requirements.txt
echo .venv/ >> .gitignore
git add vendor/kg-ssot .gitignore docs/ # and your .kg.yaml files, wherever they live
.venv/bin/python3 -I -B vendor/kg-ssot/tools/kg_gate.py --update
# 3. commit the vendor, with its stamp, and the baseline
git add .kg-ssot/gate && git commit -m "adopt the KG-SSOT contract"
Graphs that already fail become inherited debt, named by path. Step 4 is CI, on every PR. The full GitHub Actions
workflow is in section 3 of ADOPTING.md, the CI step. The step itself is:
- name: KG-SSOT contract (intact vendor + per-graph gate)
run: |
pip install --require-hashes -r vendor/kg-ssot/tools/requirements.txt
python3 -I -B vendor/kg-ssot/tools/kg_vendor.py check --dest vendor/kg-ssot
python3 -I -B vendor/kg-ssot/tools/kg_gate.py
What the gate guarantees¶
- No new graph gets in while it fails MUST.
- No inherited graph gains a MUST code it did not have.
- The SHOULD debt, by code, does not go up.
- A contract change shows up in the PR: the baseline records the name and the sha256 of the schemas.
Fixing one graph does not make up for breaking another: the gate compares MUST graph by graph.
The gate measures only .kg.yaml files. A .kg.json file in the repository stays outside it. If you want CI to
check those files, run kg_validate.py on them in a step of its own.
Output language in CI¶
Since 4.18.0, the tools print human output in English by default. They print it in Brazilian Portuguese when
KG_SSOT_LANG=pt-BR is set or the locale is pt_BR. The tools read the language in gettext order: KG_SSOT_LANG,
LANGUAGE, LC_ALL, LC_MESSAGES, LANG. The first variable that is set decides; an empty one is skipped. C,
POSIX, and a language with no catalog mean English.
Base the decision on the return code, never on the text. The gate returns rc 0 when nothing got worse, rc 1 when something got worse, and rc 2 on broken input. Here is a failing gate in English, after a new graph that fails MUST entered the PR (rc 1):
✗ WORSE docs/new.kg.yaml fails MUST: form.required.node.label
· 3 graphs; 1 pass MUST; 1 in the inherited debt
MUST docs/legacy.kg.yaml (inherited): form.required.node.plane
MUST docs/new.kg.yaml (new): form.required.node.label
With KG_SSOT_LANG=pt-BR, the same run prints ✗ PIOROU in place of ✗ WORSE. If a CI step searches the output
for text (✗ WORSE or GATE BROKEN from the gate, PASS or BROKEN INPUT from kg_validate.py), set
KG_SSOT_LANG in that step or match both languages. Data does not change language: the JSON keys, the codes, and
the place in a verdict (nó X, aresta #n) stay the same; only the message and hint text follows the language.
Update the tag¶
Do it all in one PR:
- Run
kg_vendor.py updatewith the new tag. From inside the vendor, pass--source https://github.com/marciocar/kg-ssot. - Run
git add vendor/kg-ssot. - Run
kg_vendor.py check --dest vendor/kg-ssot --indexto verify what the commit will carry. If you come from a tag older than 4.8.1, runupdatetwice: the old kit does not write the vendor's.gitattributes, and the 4.8.1 kit does. - Run
kg_gate.pyto see the difference. - Run
kg_gate.py --updateto record the new identity.
If a minor version brings a SHOULD warning that your corpus already had, the debt goes up by design. In that case,
run --update with --accept-regression "<reason>".
Bench a commit before its tag (since 4.24)¶
To test what is about to become a release, before the tag exists, bring a commit with
kg_vendor.py update --ref <commit> --source <clone or URL>. You need a repository where that commit is reachable:
the public mirror carries only tagged commits, so this is for whoever can reach the branch (a clone of the product,
a fork, a CI that checks out the PR). It takes the tree of that commit with whatever
spec/release.json it has (the version there may still be the previous one, which --tag would refuse as a
mislabeled release). The stamp says unreleased in place of the tag and keeps the commit. kg_vendor.py check
then fails with UNRELEASED VENDOR unless you pass --allow-unreleased, so a bench vendor never passes for a
release in your CI. The same goes for kg_gate.py, which also fails on a bench vendor without
--allow-unreleased and never writes the baseline from one: keep the option in the bench command, never in the CI
that guards your releases. When the tag comes out, update --tag replaces it without --force.
Move up a major version (v3 → v4)¶
v4 fails what v3 only warned about. You move up by the ramp: the graphs that start to fail enter the baseline
as inherited debt, and from then on the debt only shrinks. There is no date trick in the schema, and you do not
fix the whole corpus at once. The step-by-step guide, with the commands, is in section 5 of
ADOPTING.md, from v3 to v4.
Environment note¶
On systems with PEP 668 (Ubuntu 24.04 and later), pip install outside a venv is refused. Use a venv and run the
kit with its Python. On GitHub Actions with actions/setup-python, pip install works directly.
In an app¶
KG-SSOT holds knowledge about the product: decisions, business rules, claims, and the evidence for each one.
It is not a runtime database. A user's progress, points, and streak stay in the app's database. What goes into the
graph is, for example, "the streak resets after a day with no entry," with the source of the decision, the date it
was verified, and a TRACES_TO edge to the code or test that implements it. The recipe is in
chapter 8, "A business rule linked to code."
- In CI: the path above works for any repository, in any language. The gate only needs Python in CI.
- At runtime, in Python, through the package: since 4.9.0, the kit also ships on PyPI as
kg-ssot(pip install kg-ssot). The modules live inkg_ssot, and the package installs the commandskg-validate,kg-conformance,kg-gate,kg-store,kg-query,kg-diff,kg-mcp, andkg-prov-export. The contract and the suite travel inside the package:
from kg_ssot import kg_store
store = kg_store.GitStore("/var/lib/my-app/graphs", init=True) # outside the app's repository
The package is the path for the app. For the gate in CI, vendoring by tag is still the recommended channel,
because the contract that judges the graph stays in the repository's diff.
- At runtime, in Python, through the vendor: put vendor/kg-ssot/tools on sys.path and run without writing
bytecode, so the vendor check stays intact:
import sys
sys.dont_write_bytecode = True # or PYTHONDONTWRITEBYTECODE=1 in the environment
sys.path.insert(0, "vendor/kg-ssot/tools")
import kg_store # the store that only writes what the contract approves
store = kg_store.GitStore("/var/lib/my-app/graphs", init=True) # outside the app's repository
Keep the store root outside the app's repository. Inside a git repository, DiskStore and VersionedStore
refuse the root. GitStore with its root at the repository itself commits on whatever branch is checked out,
outside the PR, and in a subfolder of the repository it creates a nested git repository.
- In JavaScript (Node, React Native, Expo): since 4.9.0, the JS reader ships on npm as kg-ssot
(npm install kg-ssot). Its entry point imports nothing from Node, and the contract validator comes precompiled,
with no new Function and no eval, so it runs on Hermes. verdict(text, { format }) returns the MUST verdict
with the suite's codes, and the kg-ssot-validate command judges files. Since 4.10.0, it also measures SHOULD
warnings, with the same codes as the reference reader in every suite case. Line numbers and hints stay with the
Python reference reader.
- In an MCP client, such as Claude Desktop: since 4.9.0, the reference MCP server also ships as an MCP Bundle
(kg-ssot-<version>.mcpb). It is attached to the tag's release in the public repository after the kg-ssot
package of the same version is on PyPI, because the bundle installs the package from there. During install, the
client asks for the store directory and whether the server is read-only, and it installs the package with uv on
your machine. uv creates the environment inside the extension folder, locked by a uv.lock with the hash of
every dependency. The bundle carries GitHub's provenance attestation
(gh attestation verify kg-ssot-<version>.mcpb --repo marciocar/kg-ssot) and is listed in the official MCP
Registry as io.github.marciocar/kg-ssot. The store is the versioned one: every write is recorded, and
kg_history shows it. A folder inside a git repository uses git, in a nested repository (the outer repository
then sees it as an untracked folder). When the folder does not open, the server starts with judging only and
tells the model why, and the model reports it in the chat (since 4.9.1; in 4.9.0 the reason went only to the log).
It runs on macOS, Linux, and Windows. On Windows, the store runs since 4.9.1 (in 4.9.0 it did not open there),
with the path protection described in chapter 5. The server runs locally, over stdio, and talks
to no external service.
- In another language: there is no ready-made reader beyond Python and JavaScript. Validate through the CLI
(kg_validate.py --format json returns the JSON verdict of spec/verdict.schema.json) or through a model, with
the MCP server (kg_mcp.py). Your own reader proves it agrees with the contract by passing the suite
(chapter 7).
Verify a release: provenance and SBOM¶
Since 4.45.0, every artifact of a release has a software bill of materials (SBOM) in CycloneDX 1.7 JSON, next to the
provenance attestation it already had. The release on GitHub carries one <artifact>.cdx.json for the wheel, the
sdist, the npm tarball, the MCP Bundle and the OCI image, and each SBOM is also attested with the CycloneDX predicate:
gh attestation verify kg-ssot-<version>.mcpb --repo marciocar/kg-ssot # provenance
gh attestation verify kg-ssot-<version>.mcpb --repo marciocar/kg-ssot \
--predicate-type https://cyclonedx.org/bom # the SBOM
gh attestation verify oci://ghcr.io/marciocar/kg-ssot:<version> --repo marciocar/kg-ssot \
--predicate-type https://cyclonedx.org/bom
- What an SBOM lists: the dependencies that the artifact declares, read from inside it (the wheel's metadata,
the npm
package.json, the bundle'spyproject.tomlanduv.lock). No package embeds third-party code: pip, npm or uv installs the dependencies on your side, so they go withisExternal: true, with the declared range inversionRangewhen there is one. The license, the version the release was tested with (kg-ssot:tested-version) and the transitive dependencies come from the environment where the suite passed, and for npm from the build lockfile. The MCP Bundle SBOM lists every package of itsuv.lock, with the hash of each file. The image SBOM lists the base image by digest, its Debian packages, the Python interpreter and every installed Python distribution. - How it is made:
packaging/sbom/sbom.py, with the Python standard library only, in the release workflow. It refuses an artifact that embeds third-party code (node_modules,site-packages, a wheel inside a package), so the SBOM's claim that nothing is bundled is checked on every release. - Reporting a vulnerability: SECURITY.md has the supported versions, the private channel (GitHub Private Vulnerability Reporting), the response times, the scope and the disclosure policy.