@andresmassello/uscha 1.67.0 → 1.68.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/README.md +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +143 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.facts-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +143 -0
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
|
|
|
40
40
|
runtime dependencies). The npm package is a thin router; the canonical installer is
|
|
41
41
|
`uscha-kit/install-uscha.py`.
|
|
42
42
|
|
|
43
|
-
**Kit v1.
|
|
43
|
+
**Kit v1.68.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
|
@@ -76,7 +76,7 @@ and see which file, which test, and when.
|
|
|
76
76
|
| `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
|
|
77
77
|
| `/uscha-status` | One-line progress readout, in chat |
|
|
78
78
|
|
|
79
|
-
**A measurement engine** (`qa_ledger.py`,
|
|
79
|
+
**A measurement engine** (`qa_ledger.py`, 36 subcommands, Python stdlib) that ingests
|
|
80
80
|
evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
|
|
81
81
|
cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -3862,6 +3862,142 @@ def cmd_curation_check(args):
|
|
|
3862
3862
|
|
|
3863
3863
|
|
|
3864
3864
|
|
|
3865
|
+
# --------------------------------------------------------------------------- #
|
|
3866
|
+
# facts (T0 / SYSTEM-FACTS: public claims become compiled artifacts of repo
|
|
3867
|
+
# facts -- Diamond applied to Diamond. ADR-012.)
|
|
3868
|
+
# --------------------------------------------------------------------------- #
|
|
3869
|
+
|
|
3870
|
+
FACTS_FILE = "SYSTEM-FACTS.json"
|
|
3871
|
+
|
|
3872
|
+
|
|
3873
|
+
def _derive_facts():
|
|
3874
|
+
"""Facts derived from the ARTIFACTS themselves, never from prose and never from greps
|
|
3875
|
+
over documentation: the subcommand list comes from introspecting the REAL parser, the
|
|
3876
|
+
skill list from the REAL kit tree, the version from the kit VERSION file. No timestamp
|
|
3877
|
+
on purpose: regeneration over an unchanged repo must be byte-identical (AC-SF-01)."""
|
|
3878
|
+
here = os.path.abspath(__file__)
|
|
3879
|
+
# kit root by MARKER, not by fixed depth: the canonical engine sits 4 levels deep
|
|
3880
|
+
# (.claude/skills/uscha-devloop/) and the Codex twin 3 (skills/uscha-devloop/). A fixed
|
|
3881
|
+
# dirname walk made the twin silently derive the OUTER repo root -- version None,
|
|
3882
|
+
# 0 skills, no error (fresh-review HIGH, reproduced by running both copies).
|
|
3883
|
+
kit, cur = None, os.path.dirname(here)
|
|
3884
|
+
for _ in range(6):
|
|
3885
|
+
if os.path.isfile(os.path.join(cur, "VERSION")):
|
|
3886
|
+
kit = cur
|
|
3887
|
+
break
|
|
3888
|
+
nxt = os.path.dirname(cur)
|
|
3889
|
+
if nxt == cur:
|
|
3890
|
+
break
|
|
3891
|
+
cur = nxt
|
|
3892
|
+
if kit is None:
|
|
3893
|
+
print("[qa_ledger] facts: no VERSION file found walking up from the engine -- "
|
|
3894
|
+
"facts that cannot locate their own kit are not facts.", file=sys.stderr)
|
|
3895
|
+
sys.exit(2)
|
|
3896
|
+
with open(os.path.join(kit, "VERSION"), encoding="utf-8") as fh:
|
|
3897
|
+
version = fh.read().split()[-1].strip()
|
|
3898
|
+
subs = []
|
|
3899
|
+
for action in build_parser()._subparsers._group_actions:
|
|
3900
|
+
subs = sorted(action.choices.keys())
|
|
3901
|
+
skills = []
|
|
3902
|
+
for sdir in (os.path.join(kit, ".claude", "skills"), os.path.join(kit, "skills")):
|
|
3903
|
+
# canonical tree first; the Codex install ships only skills/ -- same inventory
|
|
3904
|
+
if os.path.isdir(sdir):
|
|
3905
|
+
skills = sorted(d for d in os.listdir(sdir)
|
|
3906
|
+
if os.path.isfile(os.path.join(sdir, d, "SKILL.md")))
|
|
3907
|
+
break
|
|
3908
|
+
return {
|
|
3909
|
+
"version": version,
|
|
3910
|
+
"subcommands": {"count": len(subs), "list": subs},
|
|
3911
|
+
"skills": {"count": len(skills), "list": skills},
|
|
3912
|
+
"_derivation": {
|
|
3913
|
+
"version": "uscha-kit/VERSION",
|
|
3914
|
+
"subcommands": "argparse introspection of build_parser()",
|
|
3915
|
+
"skills": "SKILL.md inventory under uscha-kit/.claude/skills/",
|
|
3916
|
+
"omitted": "stack matrix and REAL/VISION registry: no mechanical "
|
|
3917
|
+
"source exists yet -- omitted, not guessed (ADR-012)",
|
|
3918
|
+
},
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
|
|
3922
|
+
_CLAIM_PATTERNS = (
|
|
3923
|
+
# (fact key path, regex over one line, needs-context substring or None)
|
|
3924
|
+
("version", r"v(\d+\.\d+\.\d+)", "kit"),
|
|
3925
|
+
("version", r"uscha-kit\s+v?(\d+\.\d+\.\d+)", None),
|
|
3926
|
+
("subcommands.count", r"(\d+)\s+sub-?comm?ands", None),
|
|
3927
|
+
("subcommands.count", r"(\d+)\s+subcomandos", None),
|
|
3928
|
+
("skills.count", r"(\d+)\s+skills", None),
|
|
3929
|
+
)
|
|
3930
|
+
|
|
3931
|
+
|
|
3932
|
+
def _fact_value(facts, dotted):
|
|
3933
|
+
cur = facts
|
|
3934
|
+
for part in dotted.split("."):
|
|
3935
|
+
cur = cur[part]
|
|
3936
|
+
return str(cur)
|
|
3937
|
+
|
|
3938
|
+
|
|
3939
|
+
def cmd_facts(args):
|
|
3940
|
+
"""Generate SYSTEM-FACTS.json, or --check published claims against the derived facts.
|
|
3941
|
+
|
|
3942
|
+
The founding fixture (recorded in ADR-012): the site claimed kit 1.65.0 with 32 engine
|
|
3943
|
+
subcommands while the repo was at 1.67.0 with 35 -- factual drift, live, in the project
|
|
3944
|
+
about factual drift. A claim that CI does not compare against a derived fact will
|
|
3945
|
+
drift; this makes the comparison mechanical and the drift a named red."""
|
|
3946
|
+
facts = _derive_facts()
|
|
3947
|
+
if args.check:
|
|
3948
|
+
problems = []
|
|
3949
|
+
# 1) the committed facts file must match a fresh derivation (stale facts are drift)
|
|
3950
|
+
if os.path.isfile(args.out):
|
|
3951
|
+
with open(args.out, encoding="utf-8") as fh:
|
|
3952
|
+
committed = fh.read()
|
|
3953
|
+
fresh = json.dumps(facts, indent=2, ensure_ascii=False, sort_keys=True) + "\n"
|
|
3954
|
+
if committed.replace("\r\n", "\n") != fresh:
|
|
3955
|
+
problems.append((args.out, 0, "committed facts file",
|
|
3956
|
+
"stale vs regenerated", "run: qa_ledger.py facts"))
|
|
3957
|
+
else:
|
|
3958
|
+
problems.append((args.out, 0, "facts file", "absent",
|
|
3959
|
+
"run: qa_ledger.py facts"))
|
|
3960
|
+
# 2) every recognizable claim in the given files must equal the derived fact
|
|
3961
|
+
for path in args.check:
|
|
3962
|
+
try:
|
|
3963
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
3964
|
+
lines = fh.read().splitlines()
|
|
3965
|
+
except OSError as exc:
|
|
3966
|
+
problems.append((path, 0, "file", "unreadable: %s" % exc, ""))
|
|
3967
|
+
continue
|
|
3968
|
+
for n, line in enumerate(lines, 1):
|
|
3969
|
+
# an HTML comment is not a published claim -- the first live run flagged a
|
|
3970
|
+
# section marker (a comment reading "2 Skills") as a drifted count
|
|
3971
|
+
line = re.sub(r"<!--.*?-->", "", line)
|
|
3972
|
+
low = line.lower()
|
|
3973
|
+
for key, pat, ctx in _CLAIM_PATTERNS:
|
|
3974
|
+
if ctx and ctx not in low:
|
|
3975
|
+
continue
|
|
3976
|
+
for m in re.finditer(pat, line, re.I):
|
|
3977
|
+
claimed = m.group(1)
|
|
3978
|
+
actual = _fact_value(facts, key)
|
|
3979
|
+
if claimed != actual:
|
|
3980
|
+
problems.append((path, n, key, claimed, actual))
|
|
3981
|
+
if problems:
|
|
3982
|
+
print("FACTUAL DRIFT: %d claim(s) disagree with the derived facts"
|
|
3983
|
+
% len(problems))
|
|
3984
|
+
for path, n, key, claimed, actual in problems:
|
|
3985
|
+
loc = "%s:%d" % (path, n) if n else path
|
|
3986
|
+
print(" !! %s: %s claims %r, the artifact says %r"
|
|
3987
|
+
% (loc, key, claimed, actual))
|
|
3988
|
+
sys.exit(1)
|
|
3989
|
+
print("FACTS: %d file(s) checked, every claim matches the derived facts"
|
|
3990
|
+
% len(args.check))
|
|
3991
|
+
sys.exit(0)
|
|
3992
|
+
body = json.dumps(facts, indent=2, ensure_ascii=False, sort_keys=True) + "\n"
|
|
3993
|
+
with open(args.out, "w", encoding="utf-8", newline="\n") as fh:
|
|
3994
|
+
fh.write(body)
|
|
3995
|
+
print("FACTS -> %s: version %s · %d subcommands · %d skills"
|
|
3996
|
+
% (args.out, facts["version"], facts["subcommands"]["count"],
|
|
3997
|
+
facts["skills"]["count"]))
|
|
3998
|
+
|
|
3999
|
+
|
|
4000
|
+
|
|
3865
4001
|
def cmd_escalate(args):
|
|
3866
4002
|
ledger = _load(args.ledger)
|
|
3867
4003
|
_repo_node(ledger, args.repo)
|
|
@@ -7696,6 +7832,13 @@ def build_parser():
|
|
|
7696
7832
|
pcu.add_argument("--json", action="store_true")
|
|
7697
7833
|
pcu.set_defaults(func=cmd_curation_check)
|
|
7698
7834
|
|
|
7835
|
+
pfa = sub.add_parser("facts",
|
|
7836
|
+
help="SYSTEM-FACTS: derive repo facts from the artifacts, or --check published claims against them (ADR-012)")
|
|
7837
|
+
pfa.add_argument("--out", default="SYSTEM-FACTS.json")
|
|
7838
|
+
pfa.add_argument("--check", nargs="*", default=None,
|
|
7839
|
+
help="files whose claims must match the derived facts; exit 1 on drift")
|
|
7840
|
+
pfa.set_defaults(func=cmd_facts)
|
|
7841
|
+
|
|
7699
7842
|
prt = sub.add_parser("roundtrip",
|
|
7700
7843
|
help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
|
|
7701
7844
|
prt.add_argument("--ledger", default="QA-LEDGER.json")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.68.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
|
-
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py,
|
|
6
|
+
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 36 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Andres Massello",
|
|
9
9
|
"url": "https://github.com/andresmassello"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.68.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.68.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-SF-01": true, "AC-SF-02": true, "AC-SF-03": true, "AC-SF-04": true, "AC-SF-05": true}
|
|
@@ -3862,6 +3862,142 @@ def cmd_curation_check(args):
|
|
|
3862
3862
|
|
|
3863
3863
|
|
|
3864
3864
|
|
|
3865
|
+
# --------------------------------------------------------------------------- #
|
|
3866
|
+
# facts (T0 / SYSTEM-FACTS: public claims become compiled artifacts of repo
|
|
3867
|
+
# facts -- Diamond applied to Diamond. ADR-012.)
|
|
3868
|
+
# --------------------------------------------------------------------------- #
|
|
3869
|
+
|
|
3870
|
+
FACTS_FILE = "SYSTEM-FACTS.json"
|
|
3871
|
+
|
|
3872
|
+
|
|
3873
|
+
def _derive_facts():
|
|
3874
|
+
"""Facts derived from the ARTIFACTS themselves, never from prose and never from greps
|
|
3875
|
+
over documentation: the subcommand list comes from introspecting the REAL parser, the
|
|
3876
|
+
skill list from the REAL kit tree, the version from the kit VERSION file. No timestamp
|
|
3877
|
+
on purpose: regeneration over an unchanged repo must be byte-identical (AC-SF-01)."""
|
|
3878
|
+
here = os.path.abspath(__file__)
|
|
3879
|
+
# kit root by MARKER, not by fixed depth: the canonical engine sits 4 levels deep
|
|
3880
|
+
# (.claude/skills/uscha-devloop/) and the Codex twin 3 (skills/uscha-devloop/). A fixed
|
|
3881
|
+
# dirname walk made the twin silently derive the OUTER repo root -- version None,
|
|
3882
|
+
# 0 skills, no error (fresh-review HIGH, reproduced by running both copies).
|
|
3883
|
+
kit, cur = None, os.path.dirname(here)
|
|
3884
|
+
for _ in range(6):
|
|
3885
|
+
if os.path.isfile(os.path.join(cur, "VERSION")):
|
|
3886
|
+
kit = cur
|
|
3887
|
+
break
|
|
3888
|
+
nxt = os.path.dirname(cur)
|
|
3889
|
+
if nxt == cur:
|
|
3890
|
+
break
|
|
3891
|
+
cur = nxt
|
|
3892
|
+
if kit is None:
|
|
3893
|
+
print("[qa_ledger] facts: no VERSION file found walking up from the engine -- "
|
|
3894
|
+
"facts that cannot locate their own kit are not facts.", file=sys.stderr)
|
|
3895
|
+
sys.exit(2)
|
|
3896
|
+
with open(os.path.join(kit, "VERSION"), encoding="utf-8") as fh:
|
|
3897
|
+
version = fh.read().split()[-1].strip()
|
|
3898
|
+
subs = []
|
|
3899
|
+
for action in build_parser()._subparsers._group_actions:
|
|
3900
|
+
subs = sorted(action.choices.keys())
|
|
3901
|
+
skills = []
|
|
3902
|
+
for sdir in (os.path.join(kit, ".claude", "skills"), os.path.join(kit, "skills")):
|
|
3903
|
+
# canonical tree first; the Codex install ships only skills/ -- same inventory
|
|
3904
|
+
if os.path.isdir(sdir):
|
|
3905
|
+
skills = sorted(d for d in os.listdir(sdir)
|
|
3906
|
+
if os.path.isfile(os.path.join(sdir, d, "SKILL.md")))
|
|
3907
|
+
break
|
|
3908
|
+
return {
|
|
3909
|
+
"version": version,
|
|
3910
|
+
"subcommands": {"count": len(subs), "list": subs},
|
|
3911
|
+
"skills": {"count": len(skills), "list": skills},
|
|
3912
|
+
"_derivation": {
|
|
3913
|
+
"version": "uscha-kit/VERSION",
|
|
3914
|
+
"subcommands": "argparse introspection of build_parser()",
|
|
3915
|
+
"skills": "SKILL.md inventory under uscha-kit/.claude/skills/",
|
|
3916
|
+
"omitted": "stack matrix and REAL/VISION registry: no mechanical "
|
|
3917
|
+
"source exists yet -- omitted, not guessed (ADR-012)",
|
|
3918
|
+
},
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
|
|
3922
|
+
_CLAIM_PATTERNS = (
|
|
3923
|
+
# (fact key path, regex over one line, needs-context substring or None)
|
|
3924
|
+
("version", r"v(\d+\.\d+\.\d+)", "kit"),
|
|
3925
|
+
("version", r"uscha-kit\s+v?(\d+\.\d+\.\d+)", None),
|
|
3926
|
+
("subcommands.count", r"(\d+)\s+sub-?comm?ands", None),
|
|
3927
|
+
("subcommands.count", r"(\d+)\s+subcomandos", None),
|
|
3928
|
+
("skills.count", r"(\d+)\s+skills", None),
|
|
3929
|
+
)
|
|
3930
|
+
|
|
3931
|
+
|
|
3932
|
+
def _fact_value(facts, dotted):
|
|
3933
|
+
cur = facts
|
|
3934
|
+
for part in dotted.split("."):
|
|
3935
|
+
cur = cur[part]
|
|
3936
|
+
return str(cur)
|
|
3937
|
+
|
|
3938
|
+
|
|
3939
|
+
def cmd_facts(args):
|
|
3940
|
+
"""Generate SYSTEM-FACTS.json, or --check published claims against the derived facts.
|
|
3941
|
+
|
|
3942
|
+
The founding fixture (recorded in ADR-012): the site claimed kit 1.65.0 with 32 engine
|
|
3943
|
+
subcommands while the repo was at 1.67.0 with 35 -- factual drift, live, in the project
|
|
3944
|
+
about factual drift. A claim that CI does not compare against a derived fact will
|
|
3945
|
+
drift; this makes the comparison mechanical and the drift a named red."""
|
|
3946
|
+
facts = _derive_facts()
|
|
3947
|
+
if args.check:
|
|
3948
|
+
problems = []
|
|
3949
|
+
# 1) the committed facts file must match a fresh derivation (stale facts are drift)
|
|
3950
|
+
if os.path.isfile(args.out):
|
|
3951
|
+
with open(args.out, encoding="utf-8") as fh:
|
|
3952
|
+
committed = fh.read()
|
|
3953
|
+
fresh = json.dumps(facts, indent=2, ensure_ascii=False, sort_keys=True) + "\n"
|
|
3954
|
+
if committed.replace("\r\n", "\n") != fresh:
|
|
3955
|
+
problems.append((args.out, 0, "committed facts file",
|
|
3956
|
+
"stale vs regenerated", "run: qa_ledger.py facts"))
|
|
3957
|
+
else:
|
|
3958
|
+
problems.append((args.out, 0, "facts file", "absent",
|
|
3959
|
+
"run: qa_ledger.py facts"))
|
|
3960
|
+
# 2) every recognizable claim in the given files must equal the derived fact
|
|
3961
|
+
for path in args.check:
|
|
3962
|
+
try:
|
|
3963
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
3964
|
+
lines = fh.read().splitlines()
|
|
3965
|
+
except OSError as exc:
|
|
3966
|
+
problems.append((path, 0, "file", "unreadable: %s" % exc, ""))
|
|
3967
|
+
continue
|
|
3968
|
+
for n, line in enumerate(lines, 1):
|
|
3969
|
+
# an HTML comment is not a published claim -- the first live run flagged a
|
|
3970
|
+
# section marker (a comment reading "2 Skills") as a drifted count
|
|
3971
|
+
line = re.sub(r"<!--.*?-->", "", line)
|
|
3972
|
+
low = line.lower()
|
|
3973
|
+
for key, pat, ctx in _CLAIM_PATTERNS:
|
|
3974
|
+
if ctx and ctx not in low:
|
|
3975
|
+
continue
|
|
3976
|
+
for m in re.finditer(pat, line, re.I):
|
|
3977
|
+
claimed = m.group(1)
|
|
3978
|
+
actual = _fact_value(facts, key)
|
|
3979
|
+
if claimed != actual:
|
|
3980
|
+
problems.append((path, n, key, claimed, actual))
|
|
3981
|
+
if problems:
|
|
3982
|
+
print("FACTUAL DRIFT: %d claim(s) disagree with the derived facts"
|
|
3983
|
+
% len(problems))
|
|
3984
|
+
for path, n, key, claimed, actual in problems:
|
|
3985
|
+
loc = "%s:%d" % (path, n) if n else path
|
|
3986
|
+
print(" !! %s: %s claims %r, the artifact says %r"
|
|
3987
|
+
% (loc, key, claimed, actual))
|
|
3988
|
+
sys.exit(1)
|
|
3989
|
+
print("FACTS: %d file(s) checked, every claim matches the derived facts"
|
|
3990
|
+
% len(args.check))
|
|
3991
|
+
sys.exit(0)
|
|
3992
|
+
body = json.dumps(facts, indent=2, ensure_ascii=False, sort_keys=True) + "\n"
|
|
3993
|
+
with open(args.out, "w", encoding="utf-8", newline="\n") as fh:
|
|
3994
|
+
fh.write(body)
|
|
3995
|
+
print("FACTS -> %s: version %s · %d subcommands · %d skills"
|
|
3996
|
+
% (args.out, facts["version"], facts["subcommands"]["count"],
|
|
3997
|
+
facts["skills"]["count"]))
|
|
3998
|
+
|
|
3999
|
+
|
|
4000
|
+
|
|
3865
4001
|
def cmd_escalate(args):
|
|
3866
4002
|
ledger = _load(args.ledger)
|
|
3867
4003
|
_repo_node(ledger, args.repo)
|
|
@@ -7696,6 +7832,13 @@ def build_parser():
|
|
|
7696
7832
|
pcu.add_argument("--json", action="store_true")
|
|
7697
7833
|
pcu.set_defaults(func=cmd_curation_check)
|
|
7698
7834
|
|
|
7835
|
+
pfa = sub.add_parser("facts",
|
|
7836
|
+
help="SYSTEM-FACTS: derive repo facts from the artifacts, or --check published claims against them (ADR-012)")
|
|
7837
|
+
pfa.add_argument("--out", default="SYSTEM-FACTS.json")
|
|
7838
|
+
pfa.add_argument("--check", nargs="*", default=None,
|
|
7839
|
+
help="files whose claims must match the derived facts; exit 1 on drift")
|
|
7840
|
+
pfa.set_defaults(func=cmd_facts)
|
|
7841
|
+
|
|
7699
7842
|
prt = sub.add_parser("roundtrip",
|
|
7700
7843
|
help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
|
|
7701
7844
|
prt.add_argument("--ledger", default="QA-LEDGER.json")
|