Skip to content

Surface guards: config check/clean/reset, generated docs, and drift checks (3.5.0-F18, B127, B128, B129, DOC13, DEV13-DEV18) - #5

Merged
KeithRaghubar merged 16 commits into
mainfrom
feat/surface-guards
Oct 22, 2026
Merged

KeithRaghubar merged 16 commits into
mainfrom
feat/surface-guards

Conversation

@KeithRaghubar

Copy link
Copy Markdown
Owner

Ties every user-facing surface to the code: each is now either generated from it or checked against it, and CI enforces both.

User-visible

  • F18: sysforge config check / clean / reset, plus a doctor system --config axis and config check --json. Finds retired or misspelt settings in the live config files, rewrites retired ones keeping comments (--unknown also drops unknown keys), and restores keys or whole files to the shipped default (with a .bak-<time> copy). Diffs are shown first; writes need consent or --yes. The shipped defaults now ship in the wheel.
  • B128 / DOC13: the man page's ENVIRONMENT section (now 11 variables, was 3) and a new JOURNAL section are generated from tables in the code.
  • B127: bash completes run toolchain --rebuild-soname-consumers values.
  • B129: run kernel --[no-]keep-hotplug-drivers now takes effect (it was parsed and dropped).

Contributor checks

  • DEV14: completions checked per command (flags, stale flags, values, code-derived lists).
  • DEV15: citations in design docs and .claude/ resolve, including bare code names and make targets.
  • DEV16: concept-page rules.
  • DEV13: no SimpleNamespace stand-ins; tests/cli_args.py builds real parsed command lines.
  • DEV17: every parsed flag reaches the code that uses it.
  • DEV18: tests/data/surface.json snapshot with make check-surface (a new gate): stale snapshot, unrecorded surface changes and unregistered removals fail. CONTRIBUTING.md surface table, PR template, make install-hooks.

Docs fixed along the way: about 60 stale design-doc citations, a cache setting design 15 described that never existed, and two .claude files citing missing paths.

Notes for review

  • check-surface skips its base comparison on this PR, since main has no snapshot yet. It enforces from the next PR on.
  • Local: full suite green on 86445b38 (7,480 passed); the run covering the last commit (049d2149) was still going when this PR was opened, so CI is the confirmation for it.

🤖 Generated with Claude Code

KeithRaghubar and others added 16 commits October 21, 2026 19:12
New check_shipped group completion_commands. The old completions group only
walked top-level commands, so after F15 left group parents with no flags, no
subcommand flag was checked, and presence meant "anywhere in the file". Now
each command is checked where it is completed (its zsh function, minus
description text; its bash case arm): every long flag offered, nothing it
lacks offered, every argparse choice value offered, and code-derived lists
(desktop catalog, PROFILE_METHODS) equal to their constants.

B127: its first run found bash offering no values for
`run toolchain --rebuild-soname-consumers`; fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…8, DOC13)

primitives/env_vars.py lists every environment variable sysforge reads, as
user settings or diagnostic switches; the man page's ENVIRONMENT section is
generated from it. It listed 3 of 11 before, with a stale editor order and
pager flags, and SYSFORGE_PROFILE_STORE was documented nowhere.

journal.JOURNAL_FIELDS names the fields of the journal record (record_verb
now builds from its constants), and a new JOURNAL man section is generated
from it, with which commands write one and two journalctl queries.

tests/test_env_vars.py fails on an unregistered SYSFORGE_* name in the code
and on a registered variable nothing reads. gen_options keeps `[`/`|`/`-`
words off the start of a line, which scdoc would read as a table or list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upgrade never rewrites live config files, so retired settings stayed,
silently ignored, and misspelt keys were never reported.

primitives/config_schema.py is the one home for what a live file may hold:
the section/top-key schema (moved from tools/check_shipped.py, which now
imports it), the user-named open sections, keys valid because the shipped
default documents them (active or commented), and RETIRED_KEYS /
RETIRED_VALUES. The pristine defaults ship in the wheel
(sysforge/_data/config-defaults) since /etc/sysforge holds the user's copies.
Edits are line-based (comments survive), re-parsed with tomllib before
writing, and written atomically.

- config check: retired and unknown settings with their line; exits 1.
- config clean: renames retired keys (or drops one whose replacement is
  set, saying so), rewrites retired values; --unknown removes unknown ones.
- config reset FILE [KEY...]: keys back to the shipped line, or the whole
  file with a FILE.bak-<time> copy.
- doctor system --config: the same inspection in the default sweep (the
  help text's axis list and count are now pinned by a test).

The first run found toolchain.toml's reuse_unchanged and profile_store
missing from the schema; profile_store is now documented in the shipped
file, and the toolchain allowlist test accounts for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0-DEV15)

New check_standards group citations runs the claude_md resolver over
docs/design/ and the tracked .claude/ agents, skills and rules. The resolver
learned path/module.symbol citations, placeholders, symbols in any
same-named module, and make targets (a missing one counts only as a near
miss of ours); it checks paths only when their first segment is in the repo
and skips bare .sh names. Inline code is paired per line with fences
dropped, for both groups: a ``` fence used to shift every pairing after it.

Fixed what it found: design 09's prompt passage (removed partition.py and
_confirm_or_abort, inverted eof_default facts, stale per-file call counts),
build_resolved_deps under the wrong module, `make vm-install`, and stale
paths in the pkgbuild-reviewer agent and the pkgbuild-spec-check skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each concept page but the glossary closes with Further reading (one upstream
link at least) then In sysforge, and uses no sysforge vocabulary before it.
Upstream tools' flags stay allowed. The rule lived in CLAUDE.md only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cked (3.5.0-DEV15)

DEV15 skipped paths whose first segment is not in the repo, to pass over
`root/usr/bin` and `and/or`; that also skipped a citation into a top-level
directory that is gone entirely (test_missing_path_citation_is_error).
The repo's own directory names now always count as repo paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-hotplug-drivers (3.5.0-DEV13, B129)

tests/cli_args.py: cli_args(*argv, **fields) parses a real command line and
sets fields the command has, failing on any it lacks. The 31 SimpleNamespace
stand-ins for parsed arguments are converted (and the toolchain/kernel stage
ones to RunOptions); test_stand_ins keeps one deliberate case, with its
reason, in the shrinking allowlist.

What the conversion found:
- B129: `run kernel --[no-]keep-hotplug-drivers` was parsed and dropped:
  RunOptions had no field and run kernel never copied it, so the stage always
  used kernel.toml. Fixed, with test_every_run_stage_flag_reaches_the_stage
  checking every run stage's flags reach the stage.
- The sentinel-gate dry-run test exercised `build --dry-run` and
  `setup --dry-run`, which do not exist; it now covers update and run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two checks over the whole package. A field read on an options object (by
attribute or getattr with a literal) must be a RunOptions or BuildOptions
field; a missing one silently returns its default, which is 3.5.0-B129's
shape, and the check reports B129 on the pre-fix code. And every command's
parsed flag must be read through its argument object, except the dispatch
fields and two named indirect readers (doctor's _AXIS_FLAGS, pinned to the
axis flags, and PKG_KEY_BUILD_FROM_SOURCE). Both pass today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(3.5.0-DEV15)

Citations without a module (`_confirm_or_abort`) could not be resolved, so
they were never checked. Now a code-shaped name (_private, snake_case,
CamelCase) must appear in a tracked non-doc file; other software's names and
deliberate history are listed. Fixed what it found: 48 private names design
07 kept after the 3.2.0 split made them public (renamed in place), the
reconfigure editor-persist prompt (now _offer_persist_editor, writing
sysforge.toml and/or EDITOR/VISUAL), and design 15's
invalidate_on_toolchain_change, a setting that never existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es as code (3.5.0-DEV15)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efault (3.5.0-F18)

config check treats a fixed-section key as valid when the shipped default
documents it, so an undocumented key the code reads would be called unknown
and deleted by `config clean --unknown`. tests/test_config_keys_documented.py
scans fixed-section reads off config-like objects and the schema's top-level
keys, and requires each to be documented. It found profiles.toml's
extends_system (read from a user file) undocumented; it is now, in the
shipped file and the fixture.

_documented now scopes a commented example table to its own comment block,
so a top-level key documented after the header's examples stays top-level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`config check --json` prints one config-check document (config dir, and
each issue's file, line, kind, setting, message and fix) with the exit code
unchanged; tests/data/json-schema/config-check.schema.json, standards row 32,
completions and man page updated. A new test pins every json_out.emit kind
to its schema file and back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….5.0-DEV13)

Every caller passes its stage's RunOptions, which always has abi_check, so the
getattr default only ever hid a would-be rename behind a silent False, the
shape of B129. Read it directly, drop the test that fed it a stand-in to prove
the dead path, and pin _ARGS_STAND_INS empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….0-DEV18)

tools/surface.py generates tests/data/surface.json: every command's flags
and values, environment variables, journal fields, config sections and keys,
--json fields and run-log names. `make check-surface` (in GATES, so CI,
pre-release and the release preflight run it) fails when the snapshot is
stale; against the base (merge-base with main, or HEAD^ on main) it requires
every added or removed entry to be named in text the change adds to
unreleased.md, and a name that leaves the product entirely to be registered
in retired_names.toml or RETIRED_KEYS. Those were the two gaps no other check
could see.

CI's `gates` job is now a required check on main (ruleset); ci.yml and
design 17 say so. CONTRIBUTING.md gets a per-surface table naming what to
update and what enforces it, .github/pull_request_template.md a checklist,
and `make install-hooks` an optional pre-commit hook (tools/pre-commit.sh)
running the fast gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.sh's preflight now runs make check-surface; the fixture repo's stub
Makefile lacked the target, so test_fresh_release_does_not_auto_resume_from_old_tag
failed with "No rule to make target 'check-surface'".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…clean checkout (3.5.0-DEV15, 3.5.0-DEV18)

The citation check resolved `man/sysforge.1.scd` on disk. The file is a
gitignored build intermediate, present on a dev machine and absent in CI, so
the gate passed locally and failed in CI. A gitignored path now counts as
generated and is accepted either way.

CI ran twice per PR commit (push and pull_request). Push now runs only for
main, and a newer push to a PR cancels its superseded run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KeithRaghubar
KeithRaghubar merged commit b757020 into main Oct 22, 2026
1 check passed
@KeithRaghubar
KeithRaghubar deleted the feat/surface-guards branch October 22, 2026 22:52
@KeithRaghubar
KeithRaghubar restored the feat/surface-guards branch October 22, 2026 22:57
@KeithRaghubar
KeithRaghubar deleted the feat/surface-guards branch October 22, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant