@andresmassello/uscha 1.97.0 → 1.99.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 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.97.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.99.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
 
@@ -169,7 +169,7 @@ automatic tool can perform: a human verdict.
169
169
  system (= 100% drift)
170
170
 
171
171
  round trip · bench-roundtrip — how much of the asset the reverse organs re-anchor
172
- from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
172
+ from the compiled code: 0.815 measured (12 archetypes) — names AND behaviour
173
173
  ```
174
174
 
175
175
  **What each arrow is, in the engine (kit 1.96.0, 53 subcommands, all measured):**
@@ -178,25 +178,30 @@ automatic tool can perform: a human verdict.
178
178
  |---|---|---|
179
179
  | Asset → typed graph | `ir-extract`, `ir-render` | the whole package becomes one canonical IR (M2, ADR-015) — deterministic, `UNTYPED` is a measurement not an error |
180
180
  | Forward, the compiler | `compile-validate`, `compile-ingest` | any model produces code; the engine validates the output contract and never compiles (M3, ADR-016) |
181
- | Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 9 PASS · 3 PARTIAL**, three Claude-family models (Haiku · Sonnet · Opus one vendor; cross-vendor not yet measured), JS included (M4/M5, ADR-017/018/028/029) |
181
+ | Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 8 PASS · 4 PARTIAL**, four blind compilers across two vendors (Haiku · Sonnet · Opus · OpenAI Codex `gpt-5.5`), JS included (M4/M5, ADR-017/018/028/029/042) |
182
182
  | Reverse, facts | `discover`, `golden-diff` (+ the `/uscha-characterize` skill) | system map + mechanically captured golden; typed candidate observations with evidence class (M1, ADR-013) |
183
183
  | Reverse, the human gate | `curate`, `promote`, `curation-check`, `bench-curate` | one verdict per candidate, append-only ledger verified against git; unjudged → `pr-ready` blocked naming it (ADR-009/010, INV-CURATION-01) |
184
- | Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.828**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
185
-
186
- **Read the numbers the way the repo does.** 9 of 12 archetypes regenerate to the same system
187
- under an oracle the compilers never saw — that is the closed loop working. 0.828 is the mean
188
- *recoverability* of the asset from compiled code counting only static and behavioural
189
- footing. It read **0.062** until 1.90.0, with the behaviour dimension `UNMEASURED`, because no
190
- oracle case carried an AC tag a named absence, not a zero. The 12 bench oracles are now
191
- curated per case (`ORACLE-TAGS-CURATED.json`, human-authored; payloads and expectations
192
- untouched), so the dimension is measured and the number says the reverse organs anchor **names
193
- and behaviour**. Both numbers are published rather than smoothed, and what moved between them
194
- was the tagging, not the code under test. And `bench-r2` measured
184
+ | Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.815**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
185
+
186
+ **Read the numbers the way the repo does.** 8 of 12 archetypes regenerate to the same system
187
+ under an oracle the compilers never saw — that is the closed loop working. It was 9 of 12 until
188
+ 1.99.0, when a fourth compiler from a second vendor read one genuinely ambiguous sentence in
189
+ `transformer` the other way and lost a case the three Claude-family models had agreed on
190
+ (ADR-042). The number went down because the bench got harder, and the whole point of the arm was
191
+ to let it. 0.815 is the mean *recoverability* of the asset from compiled code counting only
192
+ static and behavioural footing. It read **0.062** until 1.90.0, with the behaviour dimension
193
+ `UNMEASURED`, because no oracle case carried an AC tag — a named absence, not a zero. The 12
194
+ bench oracles are now curated per case (`ORACLE-TAGS-CURATED.json`, human-authored; payloads and
195
+ expectations untouched), so the dimension is measured and the number says the reverse organs
196
+ anchor **names and behaviour**; it read 0.828 over three compilers and 0.815 over four, because
197
+ an entry's recoverability is the mean over its compilations. Every number is published rather
198
+ than smoothed, and what moved between them was the tagging and the arm, not the code under test.
199
+ And `bench-r2` measured
195
200
  that same-model reruns differ structurally about as much as different models do (aggregate
196
201
  `NOISY`) — so one earlier variance narrative was **retracted**. Every claim above is a subcommand
197
202
  you can run; every unmeasured part is labeled. That honesty is the method applied to itself.
198
203
 
199
- → The full thesis, with before/after diagrams and the REAL vs VISION table:
204
+ → The full thesis, with before/after diagrams and the REAL vs VISION vs REJECTED table:
200
205
  **[uscha.dev/diamond](https://uscha.dev/diamond)** · the mechanism, in three diagrams:
201
206
  **[uscha.dev/how](https://uscha.dev/how)**
202
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.97.0",
3
+ "version": "1.99.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",
@@ -4140,6 +4140,47 @@ CANDIDATE_DELTA_FILE = "CANDIDATE-DELTA.json" # under discovery/, machine-can
4140
4140
  CANDIDATE_DELTA_TWIN = "CANDIDATE-DELTA.md" # rendered view, regenerated, never a source
4141
4141
  CANONICAL_FILE = "CANONICAL.json" # under discovery/: the promoted package
4142
4142
  ISSUES_DEFERRED_FILE = "ISSUES-DEFERRED.md"
4143
+ # The shape of the work item `promote` writes for a `fix` verdict, and the shape it recognises
4144
+ # again on the next run. The dedupe used to be `o["id"] not in existing` -- a RAW SUBSTRING over
4145
+ # the whole file, so it suppressed the work item for any id merely MENTIONED in prose there and,
4146
+ # because the ids are a hash prefix, for any id that is a prefix of an already-written one
4147
+ # (1.69.0 fresh review, LOW, resolved 1.98.0). The question is not "does this text occur"; it is
4148
+ # "does this file already carry the WORK ITEM for this observation", and only the line shape
4149
+ # answers it. The word boundaries are what separate OBS-1 from OBS-10, and the `.*` between
4150
+ # the checkbox and the id is what lets a HUMAN reword the item -- prefix it with a date, a
4151
+ # severity, an owner -- without `promote` appending a second copy of it on the next run.
4152
+ _DEFERRED_ITEM_LINE = r"(?m)^[ \t]*[-*][ \t]*\[[ xX]\].*\b%s\b"
4153
+
4154
+
4155
+ def _deferred_carries(existing, oid):
4156
+ """True when ISSUES-DEFERRED.md already holds the `- [ ] <oid>` work item for this OBS."""
4157
+ return re.search(_DEFERRED_ITEM_LINE % re.escape(oid), existing) is not None
4158
+
4159
+
4160
+ def _config_beside_ledger(config, ledger_path):
4161
+ """Where to LOOK for a config whose default is a bare relative name.
4162
+
4163
+ `--config` defaulted to the literal `uscha.config.json`, which `os.path.isfile` resolves
4164
+ against the CWD. Run `fidelity --ledger /repo/QA-LEDGER.json` from anywhere but /repo and
4165
+ the config beside the ledger was never opened -- so `defaults.fidelity.gate` was silently
4166
+ not declared, and the INV-ADVISORY-01 refusal that reads it never fired. An UNNAMED absence
4167
+ (1.69.0 fresh review, LOW, resolved 1.98.0): the command printed a full vector and exit 0,
4168
+ exactly as it does when there genuinely is no config.
4169
+
4170
+ The cwd still WINS when it holds the file -- an explicit `--config` next to you is what you
4171
+ meant, and no existing invocation changes behaviour. Only when it does not is the ledger's
4172
+ own directory tried. Returns (path, found): callers report `path` so the answer to "which
4173
+ file did you read" is in the output rather than in the reader's head.
4174
+ """
4175
+ if os.path.isfile(config):
4176
+ return config, True
4177
+ if not os.path.isabs(config):
4178
+ beside = os.path.join(os.path.dirname(os.path.abspath(ledger_path)), config)
4179
+ if os.path.isfile(beside):
4180
+ return beside, True
4181
+ return config, False
4182
+
4183
+
4143
4184
  OBS_TYPES = ("behavior", "invariant", "contract", "config", "dependency", "decision_trace")
4144
4185
  EVIDENCE_CLASSES = ("measured", "static", "narrated")
4145
4186
  # ADR-014 / INV-ADVISORY-01: dimensions an LLM judges can only advise. The QUARANTINE is an
@@ -4445,6 +4486,17 @@ def _match_canonical(statement, canon_ids):
4445
4486
  return None
4446
4487
 
4447
4488
 
4489
+ def _md_cell(text):
4490
+ """One markdown TABLE cell. A `|` is escaped, and any CR/LF inside the value collapses to
4491
+ a space -- a markdown row ENDS at a newline, so a statement or a provenance ref carrying
4492
+ one used to split its observation across two rows and corrupt every column after it
4493
+ (1.69.0 fresh review, LOW, resolved 1.98.0). The JSON and the OBS id always survived; only
4494
+ this rendered view broke, and it is the artifact the human curates from. A space rather
4495
+ than `<br>`: every other cell is plain text, and a lone HTML tag in one column would be
4496
+ the only markup in the table."""
4497
+ return re.sub(r"[\r\n]+", " ", "%s" % text).replace("|", "\\|")
4498
+
4499
+
4448
4500
  def _render_delta_md(delta, verdicts):
4449
4501
  lines = ["<!-- %s -->" % _DELTA_BANNER, "",
4450
4502
  "# CANDIDATE-DELTA (rendered view)", ""]
@@ -4460,9 +4512,10 @@ def _render_delta_md(delta, verdicts):
4460
4512
  for o in delta["observations"]:
4461
4513
  v = verdicts.get(o["id"], "(uncurated)")
4462
4514
  files = ", ".join(o["provenance"].get("files") or []) or "-"
4463
- stmt = o["statement"].replace("|", "\\|")
4464
4515
  lines.append("| %s | %s | %s | %s | %s | %s |"
4465
- % (o["id"], o["type"], o["evidence_class"], v, stmt, files))
4516
+ % tuple(_md_cell(c) for c in (o["id"], o["type"],
4517
+ o["evidence_class"], v,
4518
+ o["statement"], files)))
4466
4519
  lines.append("")
4467
4520
  return "\n".join(lines)
4468
4521
 
@@ -4771,15 +4824,18 @@ def cmd_promote(args):
4771
4824
  if os.path.isfile(dpath):
4772
4825
  with open(dpath, encoding="utf-8-sig", errors="replace") as fh:
4773
4826
  existing = fh.read()
4774
- add = [o for o in fixes if o["id"] not in existing]
4827
+ add = [o for o in fixes if not _deferred_carries(existing, o["id"])]
4775
4828
  if add:
4776
4829
  with open(dpath, "a", encoding="utf-8", newline="\n") as fh:
4777
4830
  if existing and not existing.endswith("\n"):
4778
4831
  fh.write("\n")
4779
4832
  for o in add:
4833
+ # a markdown checklist item ends at a newline just as a table row
4834
+ # does: a multi-line statement would split the work item in two and
4835
+ # leave the half _deferred_carries recognises without its text.
4780
4836
  fh.write("- [ ] %s (curated `fix`): %s -- observed behavior the human "
4781
4837
  "ruled a defect; NEVER canonical (ADR-013)\n"
4782
- % (o["id"], o["statement"]))
4838
+ % (o["id"], _md_cell(o["statement"])))
4783
4839
  new_fix = [o["id"] for o in add]
4784
4840
  ledger["candidate_delta"] = {"repo": args.repo, "total": len(obs),
4785
4841
  "curated": len(obs),
@@ -4817,16 +4873,20 @@ def cmd_fidelity(args):
4817
4873
  node = _repo_node(ledger, args.repo)
4818
4874
  repo_path = _scope_path(ledger, args.repo)
4819
4875
  cfg = {}
4820
- if os.path.isfile(args.config):
4876
+ # WHERE the config was looked for is part of the answer: a gate declared in a file the
4877
+ # command never opened is a gate that does not exist, and the old cwd-only resolution said
4878
+ # nothing about it (1.69.0 fresh review, resolved 1.98.0).
4879
+ cfg_path, cfg_found = _config_beside_ledger(args.config, args.ledger)
4880
+ if cfg_found:
4821
4881
  try:
4822
- with open(args.config, encoding="utf-8-sig") as fh:
4882
+ with open(cfg_path, encoding="utf-8-sig") as fh:
4823
4883
  cfg = json.load(fh)
4824
4884
  except (OSError, ValueError) as exc:
4825
4885
  # a config that cannot be parsed cannot declare gates -- swallowing the error
4826
4886
  # would DISABLE the INV-ADVISORY-01 refusal on a syntax slip (fresh-review
4827
4887
  # HIGH, reproduced). Malformation is exit 2, never a silent degrade.
4828
4888
  print("[qa_ledger] fidelity: %s unreadable: %s -- refusing to guess what it "
4829
- "declares." % (args.config, exc), file=sys.stderr)
4889
+ "declares." % (cfg_path, exc), file=sys.stderr)
4830
4890
  sys.exit(2)
4831
4891
  declared = ((cfg.get("defaults") or {}).get("fidelity") or {}).get("gate") or []
4832
4892
  for dim in (ledger.get("config", {}).get("defaults", {}).get("fidelity")
@@ -4979,6 +5039,7 @@ def cmd_fidelity(args):
4979
5039
  dims["semantic"] = _fid_dim(None, "not wired: an LLM-judged comparison enters as "
4980
5040
  "advisory only and can NEVER gate (INV-ADVISORY-01)")
4981
5041
  out = {"repo": args.repo,
5042
+ "config": cfg_path if cfg_found else None,
4982
5043
  **({"path": bound} if bound else {}),
4983
5044
  "dimensions": {k: dict(dims[k], **{"class": FIDELITY_DIMENSIONS[k]})
4984
5045
  for k in ("traceability", "behavior", "contracts",
@@ -4990,6 +5051,11 @@ def cmd_fidelity(args):
4990
5051
  else:
4991
5052
  print("FIDELITY %s%s (vector -- no blend; each number stands on its own evidence):"
4992
5053
  % (args.repo, " [bounded to %s]" % bound if bound else ""))
5054
+ if cfg_found:
5055
+ print(" config %s" % cfg_path)
5056
+ else:
5057
+ print(" config none (looked for %r in the cwd and beside the ledger) "
5058
+ "-- no fidelity gate declared from a file" % args.config)
4993
5059
  for k, d in out["dimensions"].items():
4994
5060
  val = "UNMEASURED" if d["value"] is None else "%.2f" % d["value"]
4995
5061
  print(" %-17s %-10s [%s] %s" % (k, val, d["class"], d["provenance"]))
@@ -5211,8 +5277,8 @@ def _render_ir_md(graph):
5211
5277
  "|----|------|-----------|--------|"]
5212
5278
  for nd in graph.get("nodes") or []:
5213
5279
  src = "%s:%s" % (nd["source"]["file"], nd["source"]["line"])
5214
- stmt = (nd.get("statement") or "").replace("|", "\\|")
5215
- lines.append("| %s | %s | %s | %s |" % (nd["id"], nd["type"], stmt, src))
5280
+ lines.append("| %s | %s | %s | %s |"
5281
+ % (nd["id"], nd["type"], _md_cell(nd.get("statement") or ""), src))
5216
5282
  lines += ["", "## Edges", "", "| from | type | to |", "|------|------|----|"]
5217
5283
  for e in graph.get("edges") or []:
5218
5284
  lines.append("| %s | %s | %s |" % (e["from"], e["type"], e["to"]))
@@ -5221,8 +5287,11 @@ def _render_ir_md(graph):
5221
5287
  "| text | source | reason |", "|------|--------|--------|"]
5222
5288
  for u in graph["untyped"]:
5223
5289
  src = "%s:%s" % (u["source"]["file"], u["source"]["line"])
5224
- txt = (u.get("text") or "").replace("|", "\\|")[:80]
5225
- lines.append("| %s | %s | %s |" % (txt, src, u.get("reason", "")))
5290
+ # truncate AFTER the cell is flattened: slicing raw text can cut mid-newline
5291
+ # and leave the break inside the 80 characters that reach the row.
5292
+ txt = _md_cell(u.get("text") or "")[:80]
5293
+ lines.append("| %s | %s | %s |"
5294
+ % (txt, src, _md_cell(u.get("reason", ""))))
5226
5295
  lines.append("")
5227
5296
  return "\n".join(lines)
5228
5297
 
@@ -11189,18 +11258,33 @@ def _lc_valid_date(s):
11189
11258
  return False
11190
11259
  _LC_KEY = re.compile(r"^lifecycle\s*:\s*(.*)$")
11191
11260
  _LC_GOLIVE_FM = re.compile(r"^go[-_ ]?live\s*:\s*(.+?)\s*$", re.I)
11192
- _LC_GOLIVE_LINE = re.compile(r"^\s*\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
11261
+ # the body form. A list marker or a blockquote prefix in front of the bold label is still the
11262
+ # same declaration -- a SPEC that writes it as a bullet used to read as "no go-live declared"
11263
+ # (1.98.1, field report). The prefix is markdown-CORRECT, not merely permissive: at most three
11264
+ # leading spaces and never a tab, because four spaces or a tab opens an INDENTED CODE BLOCK --
11265
+ # an example, not a declaration -- and a list marker must be FOLLOWED by whitespace, so the
11266
+ # glued `-**Go-live:**` is not a list item. Fenced blocks are skipped by the caller for the
11267
+ # same reason. The trailing \b keeps the date a WHOLE token: it must be followed by a
11268
+ # non-digit or the end of the line, so a longer number can never be truncated into a date,
11269
+ # while ordinary prose after it ("(delivery 1). ...") is allowed.
11270
+ _LC_GOLIVE_LINE = re.compile(r"^ {0,3}(?:> ?)*(?:(?:[-*+]|\d+[.)])\s+)?"
11271
+ r"\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
11193
11272
  r"(\d{4}-\d{2}-\d{2})\b", re.I)
11194
11273
  _LC_FIELDS = ("component", "version", "eol", "source", "checked")
11195
11274
 
11196
11275
 
11197
11276
  def _lc_frontmatter(lines):
11198
11277
  """The lines INSIDE a leading `---` frontmatter block, or None when there is none.
11278
+ Leading blank lines and a BOM are skipped before the fence is looked for: a file whose
11279
+ text reached us with anything glued in front still opens its block at the top (1.98.1).
11199
11280
  An unterminated fence is not data: a half-written block reads as absent."""
11200
- if not lines or lines[0].strip() != "---":
11281
+ i = 0
11282
+ while i < len(lines) and not lines[i].lstrip("\ufeff").strip():
11283
+ i += 1
11284
+ if i >= len(lines) or lines[i].lstrip("\ufeff").strip() != "---":
11201
11285
  return None
11202
11286
  body = []
11203
- for ln in lines[1:]:
11287
+ for ln in lines[i + 1:]:
11204
11288
  if ln.strip() == "---":
11205
11289
  return body
11206
11290
  body.append(ln)
@@ -11252,7 +11336,8 @@ def _lc_parse(lines):
11252
11336
 
11253
11337
  def _lc_go_live(text):
11254
11338
  """The declared go-live of a SPEC: frontmatter `go_live: YYYY-MM-DD`, or a
11255
- `**Go-live:** YYYY-MM-DD` line anywhere in the body. None = not declared."""
11339
+ `**Go-live:** YYYY-MM-DD` line anywhere in the body OUTSIDE a fenced block.
11340
+ None = not declared."""
11256
11341
  lines = [ln.rstrip("\r") for ln in (text or "").split("\n")]
11257
11342
  fm = _lc_frontmatter(lines)
11258
11343
  if fm:
@@ -11264,7 +11349,14 @@ def _lc_go_live(text):
11264
11349
  v = m.group(1).strip().strip("\x27\x22")
11265
11350
  if _lc_valid_date(v):
11266
11351
  return v
11352
+ in_fence = False
11267
11353
  for ln in lines:
11354
+ st = ln.strip()
11355
+ if st.startswith("```") or st.startswith("~~~"):
11356
+ in_fence = not in_fence
11357
+ continue
11358
+ if in_fence: # a SPEC that QUOTES the form is showing it, not declaring a date --
11359
+ continue # and the quoted line comes FIRST (the fence idiom of _spec_check_text)
11268
11360
  m = _LC_GOLIVE_LINE.match(ln)
11269
11361
  if m and _lc_valid_date(m.group(1)):
11270
11362
  return m.group(1)
@@ -11376,7 +11468,12 @@ def _lifecycle_for(root, adr_dir=None, spec_text=None, fallback=True):
11376
11468
  if os.path.isfile(sp):
11377
11469
  try:
11378
11470
  with open(sp, "r", encoding="utf-8", errors="replace") as fh:
11379
- spec_text = (spec_text or "") + "\n" + fh.read()
11471
+ body = fh.read()
11472
+ # join only when there is something to join TO. Gluing a newline in front of
11473
+ # the file pushed its `---` off line 0, so the frontmatter went invisible and
11474
+ # `readiness` read "no go-live declared" on a SPEC that `spec-check` measured
11475
+ # (1.98.1, field report).
11476
+ spec_text = (spec_text + "\n" + body) if spec_text else body
11380
11477
  except OSError:
11381
11478
  pass
11382
11479
  return _lifecycle_report(adr_dir or os.path.join(root, "docs", "adr"), spec_text)
@@ -12500,7 +12597,9 @@ def build_parser():
12500
12597
  pfv.add_argument("--ledger", default="QA-LEDGER.json")
12501
12598
  pfv.add_argument("--repo", required=True)
12502
12599
  pfv.add_argument("--config", default="uscha.config.json",
12503
- help="checked for defaults.fidelity.gate -- advisory there is a refusal")
12600
+ help="checked for defaults.fidelity.gate -- advisory there is a "
12601
+ "refusal. A relative name is resolved against the cwd first, "
12602
+ "then beside --ledger; the path actually read is reported")
12504
12603
  pfv.add_argument("--ir", action="store_true",
12505
12604
  help="answer curation_closure as a path query over the IR graph "
12506
12605
  "(ADR-015); reproduces v0 from the derived index")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "1.97.0",
4
+ "version": "1.99.0",
5
5
  "displayName": "Uscha",
6
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, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
7
7
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.97.0",
3
+ "version": "1.99.0",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.97.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.99.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.97.0
1
+ uscha-kit 1.99.0
@@ -4140,6 +4140,47 @@ CANDIDATE_DELTA_FILE = "CANDIDATE-DELTA.json" # under discovery/, machine-can
4140
4140
  CANDIDATE_DELTA_TWIN = "CANDIDATE-DELTA.md" # rendered view, regenerated, never a source
4141
4141
  CANONICAL_FILE = "CANONICAL.json" # under discovery/: the promoted package
4142
4142
  ISSUES_DEFERRED_FILE = "ISSUES-DEFERRED.md"
4143
+ # The shape of the work item `promote` writes for a `fix` verdict, and the shape it recognises
4144
+ # again on the next run. The dedupe used to be `o["id"] not in existing` -- a RAW SUBSTRING over
4145
+ # the whole file, so it suppressed the work item for any id merely MENTIONED in prose there and,
4146
+ # because the ids are a hash prefix, for any id that is a prefix of an already-written one
4147
+ # (1.69.0 fresh review, LOW, resolved 1.98.0). The question is not "does this text occur"; it is
4148
+ # "does this file already carry the WORK ITEM for this observation", and only the line shape
4149
+ # answers it. The word boundaries are what separate OBS-1 from OBS-10, and the `.*` between
4150
+ # the checkbox and the id is what lets a HUMAN reword the item -- prefix it with a date, a
4151
+ # severity, an owner -- without `promote` appending a second copy of it on the next run.
4152
+ _DEFERRED_ITEM_LINE = r"(?m)^[ \t]*[-*][ \t]*\[[ xX]\].*\b%s\b"
4153
+
4154
+
4155
+ def _deferred_carries(existing, oid):
4156
+ """True when ISSUES-DEFERRED.md already holds the `- [ ] <oid>` work item for this OBS."""
4157
+ return re.search(_DEFERRED_ITEM_LINE % re.escape(oid), existing) is not None
4158
+
4159
+
4160
+ def _config_beside_ledger(config, ledger_path):
4161
+ """Where to LOOK for a config whose default is a bare relative name.
4162
+
4163
+ `--config` defaulted to the literal `uscha.config.json`, which `os.path.isfile` resolves
4164
+ against the CWD. Run `fidelity --ledger /repo/QA-LEDGER.json` from anywhere but /repo and
4165
+ the config beside the ledger was never opened -- so `defaults.fidelity.gate` was silently
4166
+ not declared, and the INV-ADVISORY-01 refusal that reads it never fired. An UNNAMED absence
4167
+ (1.69.0 fresh review, LOW, resolved 1.98.0): the command printed a full vector and exit 0,
4168
+ exactly as it does when there genuinely is no config.
4169
+
4170
+ The cwd still WINS when it holds the file -- an explicit `--config` next to you is what you
4171
+ meant, and no existing invocation changes behaviour. Only when it does not is the ledger's
4172
+ own directory tried. Returns (path, found): callers report `path` so the answer to "which
4173
+ file did you read" is in the output rather than in the reader's head.
4174
+ """
4175
+ if os.path.isfile(config):
4176
+ return config, True
4177
+ if not os.path.isabs(config):
4178
+ beside = os.path.join(os.path.dirname(os.path.abspath(ledger_path)), config)
4179
+ if os.path.isfile(beside):
4180
+ return beside, True
4181
+ return config, False
4182
+
4183
+
4143
4184
  OBS_TYPES = ("behavior", "invariant", "contract", "config", "dependency", "decision_trace")
4144
4185
  EVIDENCE_CLASSES = ("measured", "static", "narrated")
4145
4186
  # ADR-014 / INV-ADVISORY-01: dimensions an LLM judges can only advise. The QUARANTINE is an
@@ -4445,6 +4486,17 @@ def _match_canonical(statement, canon_ids):
4445
4486
  return None
4446
4487
 
4447
4488
 
4489
+ def _md_cell(text):
4490
+ """One markdown TABLE cell. A `|` is escaped, and any CR/LF inside the value collapses to
4491
+ a space -- a markdown row ENDS at a newline, so a statement or a provenance ref carrying
4492
+ one used to split its observation across two rows and corrupt every column after it
4493
+ (1.69.0 fresh review, LOW, resolved 1.98.0). The JSON and the OBS id always survived; only
4494
+ this rendered view broke, and it is the artifact the human curates from. A space rather
4495
+ than `<br>`: every other cell is plain text, and a lone HTML tag in one column would be
4496
+ the only markup in the table."""
4497
+ return re.sub(r"[\r\n]+", " ", "%s" % text).replace("|", "\\|")
4498
+
4499
+
4448
4500
  def _render_delta_md(delta, verdicts):
4449
4501
  lines = ["<!-- %s -->" % _DELTA_BANNER, "",
4450
4502
  "# CANDIDATE-DELTA (rendered view)", ""]
@@ -4460,9 +4512,10 @@ def _render_delta_md(delta, verdicts):
4460
4512
  for o in delta["observations"]:
4461
4513
  v = verdicts.get(o["id"], "(uncurated)")
4462
4514
  files = ", ".join(o["provenance"].get("files") or []) or "-"
4463
- stmt = o["statement"].replace("|", "\\|")
4464
4515
  lines.append("| %s | %s | %s | %s | %s | %s |"
4465
- % (o["id"], o["type"], o["evidence_class"], v, stmt, files))
4516
+ % tuple(_md_cell(c) for c in (o["id"], o["type"],
4517
+ o["evidence_class"], v,
4518
+ o["statement"], files)))
4466
4519
  lines.append("")
4467
4520
  return "\n".join(lines)
4468
4521
 
@@ -4771,15 +4824,18 @@ def cmd_promote(args):
4771
4824
  if os.path.isfile(dpath):
4772
4825
  with open(dpath, encoding="utf-8-sig", errors="replace") as fh:
4773
4826
  existing = fh.read()
4774
- add = [o for o in fixes if o["id"] not in existing]
4827
+ add = [o for o in fixes if not _deferred_carries(existing, o["id"])]
4775
4828
  if add:
4776
4829
  with open(dpath, "a", encoding="utf-8", newline="\n") as fh:
4777
4830
  if existing and not existing.endswith("\n"):
4778
4831
  fh.write("\n")
4779
4832
  for o in add:
4833
+ # a markdown checklist item ends at a newline just as a table row
4834
+ # does: a multi-line statement would split the work item in two and
4835
+ # leave the half _deferred_carries recognises without its text.
4780
4836
  fh.write("- [ ] %s (curated `fix`): %s -- observed behavior the human "
4781
4837
  "ruled a defect; NEVER canonical (ADR-013)\n"
4782
- % (o["id"], o["statement"]))
4838
+ % (o["id"], _md_cell(o["statement"])))
4783
4839
  new_fix = [o["id"] for o in add]
4784
4840
  ledger["candidate_delta"] = {"repo": args.repo, "total": len(obs),
4785
4841
  "curated": len(obs),
@@ -4817,16 +4873,20 @@ def cmd_fidelity(args):
4817
4873
  node = _repo_node(ledger, args.repo)
4818
4874
  repo_path = _scope_path(ledger, args.repo)
4819
4875
  cfg = {}
4820
- if os.path.isfile(args.config):
4876
+ # WHERE the config was looked for is part of the answer: a gate declared in a file the
4877
+ # command never opened is a gate that does not exist, and the old cwd-only resolution said
4878
+ # nothing about it (1.69.0 fresh review, resolved 1.98.0).
4879
+ cfg_path, cfg_found = _config_beside_ledger(args.config, args.ledger)
4880
+ if cfg_found:
4821
4881
  try:
4822
- with open(args.config, encoding="utf-8-sig") as fh:
4882
+ with open(cfg_path, encoding="utf-8-sig") as fh:
4823
4883
  cfg = json.load(fh)
4824
4884
  except (OSError, ValueError) as exc:
4825
4885
  # a config that cannot be parsed cannot declare gates -- swallowing the error
4826
4886
  # would DISABLE the INV-ADVISORY-01 refusal on a syntax slip (fresh-review
4827
4887
  # HIGH, reproduced). Malformation is exit 2, never a silent degrade.
4828
4888
  print("[qa_ledger] fidelity: %s unreadable: %s -- refusing to guess what it "
4829
- "declares." % (args.config, exc), file=sys.stderr)
4889
+ "declares." % (cfg_path, exc), file=sys.stderr)
4830
4890
  sys.exit(2)
4831
4891
  declared = ((cfg.get("defaults") or {}).get("fidelity") or {}).get("gate") or []
4832
4892
  for dim in (ledger.get("config", {}).get("defaults", {}).get("fidelity")
@@ -4979,6 +5039,7 @@ def cmd_fidelity(args):
4979
5039
  dims["semantic"] = _fid_dim(None, "not wired: an LLM-judged comparison enters as "
4980
5040
  "advisory only and can NEVER gate (INV-ADVISORY-01)")
4981
5041
  out = {"repo": args.repo,
5042
+ "config": cfg_path if cfg_found else None,
4982
5043
  **({"path": bound} if bound else {}),
4983
5044
  "dimensions": {k: dict(dims[k], **{"class": FIDELITY_DIMENSIONS[k]})
4984
5045
  for k in ("traceability", "behavior", "contracts",
@@ -4990,6 +5051,11 @@ def cmd_fidelity(args):
4990
5051
  else:
4991
5052
  print("FIDELITY %s%s (vector -- no blend; each number stands on its own evidence):"
4992
5053
  % (args.repo, " [bounded to %s]" % bound if bound else ""))
5054
+ if cfg_found:
5055
+ print(" config %s" % cfg_path)
5056
+ else:
5057
+ print(" config none (looked for %r in the cwd and beside the ledger) "
5058
+ "-- no fidelity gate declared from a file" % args.config)
4993
5059
  for k, d in out["dimensions"].items():
4994
5060
  val = "UNMEASURED" if d["value"] is None else "%.2f" % d["value"]
4995
5061
  print(" %-17s %-10s [%s] %s" % (k, val, d["class"], d["provenance"]))
@@ -5211,8 +5277,8 @@ def _render_ir_md(graph):
5211
5277
  "|----|------|-----------|--------|"]
5212
5278
  for nd in graph.get("nodes") or []:
5213
5279
  src = "%s:%s" % (nd["source"]["file"], nd["source"]["line"])
5214
- stmt = (nd.get("statement") or "").replace("|", "\\|")
5215
- lines.append("| %s | %s | %s | %s |" % (nd["id"], nd["type"], stmt, src))
5280
+ lines.append("| %s | %s | %s | %s |"
5281
+ % (nd["id"], nd["type"], _md_cell(nd.get("statement") or ""), src))
5216
5282
  lines += ["", "## Edges", "", "| from | type | to |", "|------|------|----|"]
5217
5283
  for e in graph.get("edges") or []:
5218
5284
  lines.append("| %s | %s | %s |" % (e["from"], e["type"], e["to"]))
@@ -5221,8 +5287,11 @@ def _render_ir_md(graph):
5221
5287
  "| text | source | reason |", "|------|--------|--------|"]
5222
5288
  for u in graph["untyped"]:
5223
5289
  src = "%s:%s" % (u["source"]["file"], u["source"]["line"])
5224
- txt = (u.get("text") or "").replace("|", "\\|")[:80]
5225
- lines.append("| %s | %s | %s |" % (txt, src, u.get("reason", "")))
5290
+ # truncate AFTER the cell is flattened: slicing raw text can cut mid-newline
5291
+ # and leave the break inside the 80 characters that reach the row.
5292
+ txt = _md_cell(u.get("text") or "")[:80]
5293
+ lines.append("| %s | %s | %s |"
5294
+ % (txt, src, _md_cell(u.get("reason", ""))))
5226
5295
  lines.append("")
5227
5296
  return "\n".join(lines)
5228
5297
 
@@ -11189,18 +11258,33 @@ def _lc_valid_date(s):
11189
11258
  return False
11190
11259
  _LC_KEY = re.compile(r"^lifecycle\s*:\s*(.*)$")
11191
11260
  _LC_GOLIVE_FM = re.compile(r"^go[-_ ]?live\s*:\s*(.+?)\s*$", re.I)
11192
- _LC_GOLIVE_LINE = re.compile(r"^\s*\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
11261
+ # the body form. A list marker or a blockquote prefix in front of the bold label is still the
11262
+ # same declaration -- a SPEC that writes it as a bullet used to read as "no go-live declared"
11263
+ # (1.98.1, field report). The prefix is markdown-CORRECT, not merely permissive: at most three
11264
+ # leading spaces and never a tab, because four spaces or a tab opens an INDENTED CODE BLOCK --
11265
+ # an example, not a declaration -- and a list marker must be FOLLOWED by whitespace, so the
11266
+ # glued `-**Go-live:**` is not a list item. Fenced blocks are skipped by the caller for the
11267
+ # same reason. The trailing \b keeps the date a WHOLE token: it must be followed by a
11268
+ # non-digit or the end of the line, so a longer number can never be truncated into a date,
11269
+ # while ordinary prose after it ("(delivery 1). ...") is allowed.
11270
+ _LC_GOLIVE_LINE = re.compile(r"^ {0,3}(?:> ?)*(?:(?:[-*+]|\d+[.)])\s+)?"
11271
+ r"\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
11193
11272
  r"(\d{4}-\d{2}-\d{2})\b", re.I)
11194
11273
  _LC_FIELDS = ("component", "version", "eol", "source", "checked")
11195
11274
 
11196
11275
 
11197
11276
  def _lc_frontmatter(lines):
11198
11277
  """The lines INSIDE a leading `---` frontmatter block, or None when there is none.
11278
+ Leading blank lines and a BOM are skipped before the fence is looked for: a file whose
11279
+ text reached us with anything glued in front still opens its block at the top (1.98.1).
11199
11280
  An unterminated fence is not data: a half-written block reads as absent."""
11200
- if not lines or lines[0].strip() != "---":
11281
+ i = 0
11282
+ while i < len(lines) and not lines[i].lstrip("\ufeff").strip():
11283
+ i += 1
11284
+ if i >= len(lines) or lines[i].lstrip("\ufeff").strip() != "---":
11201
11285
  return None
11202
11286
  body = []
11203
- for ln in lines[1:]:
11287
+ for ln in lines[i + 1:]:
11204
11288
  if ln.strip() == "---":
11205
11289
  return body
11206
11290
  body.append(ln)
@@ -11252,7 +11336,8 @@ def _lc_parse(lines):
11252
11336
 
11253
11337
  def _lc_go_live(text):
11254
11338
  """The declared go-live of a SPEC: frontmatter `go_live: YYYY-MM-DD`, or a
11255
- `**Go-live:** YYYY-MM-DD` line anywhere in the body. None = not declared."""
11339
+ `**Go-live:** YYYY-MM-DD` line anywhere in the body OUTSIDE a fenced block.
11340
+ None = not declared."""
11256
11341
  lines = [ln.rstrip("\r") for ln in (text or "").split("\n")]
11257
11342
  fm = _lc_frontmatter(lines)
11258
11343
  if fm:
@@ -11264,7 +11349,14 @@ def _lc_go_live(text):
11264
11349
  v = m.group(1).strip().strip("\x27\x22")
11265
11350
  if _lc_valid_date(v):
11266
11351
  return v
11352
+ in_fence = False
11267
11353
  for ln in lines:
11354
+ st = ln.strip()
11355
+ if st.startswith("```") or st.startswith("~~~"):
11356
+ in_fence = not in_fence
11357
+ continue
11358
+ if in_fence: # a SPEC that QUOTES the form is showing it, not declaring a date --
11359
+ continue # and the quoted line comes FIRST (the fence idiom of _spec_check_text)
11268
11360
  m = _LC_GOLIVE_LINE.match(ln)
11269
11361
  if m and _lc_valid_date(m.group(1)):
11270
11362
  return m.group(1)
@@ -11376,7 +11468,12 @@ def _lifecycle_for(root, adr_dir=None, spec_text=None, fallback=True):
11376
11468
  if os.path.isfile(sp):
11377
11469
  try:
11378
11470
  with open(sp, "r", encoding="utf-8", errors="replace") as fh:
11379
- spec_text = (spec_text or "") + "\n" + fh.read()
11471
+ body = fh.read()
11472
+ # join only when there is something to join TO. Gluing a newline in front of
11473
+ # the file pushed its `---` off line 0, so the frontmatter went invisible and
11474
+ # `readiness` read "no go-live declared" on a SPEC that `spec-check` measured
11475
+ # (1.98.1, field report).
11476
+ spec_text = (spec_text + "\n" + body) if spec_text else body
11380
11477
  except OSError:
11381
11478
  pass
11382
11479
  return _lifecycle_report(adr_dir or os.path.join(root, "docs", "adr"), spec_text)
@@ -12500,7 +12597,9 @@ def build_parser():
12500
12597
  pfv.add_argument("--ledger", default="QA-LEDGER.json")
12501
12598
  pfv.add_argument("--repo", required=True)
12502
12599
  pfv.add_argument("--config", default="uscha.config.json",
12503
- help="checked for defaults.fidelity.gate -- advisory there is a refusal")
12600
+ help="checked for defaults.fidelity.gate -- advisory there is a "
12601
+ "refusal. A relative name is resolved against the cwd first, "
12602
+ "then beside --ledger; the path actually read is reported")
12504
12603
  pfv.add_argument("--ir", action="store_true",
12505
12604
  help="answer curation_closure as a path query over the IR graph "
12506
12605
  "(ADR-015); reproduces v0 from the derived index")
@@ -121,7 +121,7 @@ enforcing the record is the engine's job. It is never resolved by "working aroun
121
121
  - [ ] **Runs without the human typing anything** — a script/agent autocompletes it; no routine forms
122
122
  - [ ] **Speaks only when it matters** — a failure, or a high-risk profile; if it always speaks, it is silenced by default
123
123
  - [ ] **Collapses into `readiness`** — one number + one line, not another screen (`--verbose` opens the detail)
124
- - [ ] **A trivial change skips it** — gated by risk profile <!-- principle: profiles A–E NOT yet mechanized in the engine -->
124
+ - [ ] **A trivial change skips it** — gated by risk profile <!-- principle: profiles A–E are NOT mechanized in the engine, by design -- the paragraph above says why: this is design discipline and review judgment, not a check -->
125
125
 
126
126
  ## How it is enforced
127
127
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.97.0",
2
+ "version": "1.99.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,