@andresmassello/uscha 1.93.1 → 1.94.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-adr-refine/SKILL.md +30 -6
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +245 -1
- package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +31 -7
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- 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/skills/uscha-adr-refine/SKILL.md +30 -6
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +245 -1
- package/uscha-kit/skills/uscha-discovery/SKILL.md +31 -7
- package/uscha-kit/templates/CONSTITUTION.md +17 -0
- package/uscha-kit/templates/docs/adr/ADR-stack-template.md +73 -0
- package/uscha-kit/templates/docs/adr/README.md +5 -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.94.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
|
|
|
@@ -85,7 +85,7 @@ automatic tool can perform: a human verdict.
|
|
|
85
85
|
from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
**What each arrow is, in the engine (kit 1.
|
|
88
|
+
**What each arrow is, in the engine (kit 1.94.0, 53 subcommands, all measured):**
|
|
89
89
|
|
|
90
90
|
| Leg | Subcommands | What it establishes |
|
|
91
91
|
|---|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.94.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",
|
|
@@ -112,21 +112,45 @@ it's already fully answered. Keep a running list of OPEN GAPS and resolved decis
|
|
|
112
112
|
2. **Implicit decisions.** Surface the choices the request assumed: sync vs async,
|
|
113
113
|
storage, protocol, idempotency, transactional boundaries, who owns state. For each,
|
|
114
114
|
force an explicit decision and at least one considered alternative.
|
|
115
|
-
3. **
|
|
115
|
+
3. **Stack and lifecycle (MANDATORY — before any stack/architecture decision is
|
|
116
|
+
recorded).** The stack is not a given ("we use X"): it is a decision with an EXPIRY
|
|
117
|
+
DATE. A major version is a family; support is granted to a MINOR line, for a window,
|
|
118
|
+
by an upstream nobody in the room controls. Ask these one at a time, each with your
|
|
119
|
+
recommended answer, and **FETCH the dates from the official source AS YOU ASK — never
|
|
120
|
+
answer from memory**; record the URL and the day you checked:
|
|
121
|
+
(a) the EXACT version of every runtime/framework/store (JDK, web framework, ORM, DB,
|
|
122
|
+
Node, bundler, broker, cache) and its OSS/LTS end-of-support date, cited;
|
|
123
|
+
(b) support window vs the declared go-live AND the expected operating life — does the
|
|
124
|
+
line stay patched for the whole operation? If not, move it up BEFORE building: a major
|
|
125
|
+
upgrade days before the milestone is the most expensive one there is;
|
|
126
|
+
(c) major dependencies and the upgrade policy — who approves one, and when it is
|
|
127
|
+
scheduled (aligned with the dev-loop's "zero new dependencies without approval");
|
|
128
|
+
(d) development and observability tools the operator wants from day one (consoles, APM,
|
|
129
|
+
admin UIs) — they CONSTRAIN versions, so ask early or they force the upgrade;
|
|
130
|
+
(e) compatibility with reused legacy modules — the minimum version they support.
|
|
131
|
+
The answers distil into the stack ADR with the dates INSIDE it, as the machine-readable
|
|
132
|
+
`lifecycle:` frontmatter block (`component` / `version` / `eol` / `source` / `checked`,
|
|
133
|
+
ISO dates; `eol: unknown` is allowed and reads as a NAMED absence, never a pass) — see
|
|
134
|
+
`templates/docs/adr/ADR-stack-template.md`. The SPEC declares the milestone it is
|
|
135
|
+
compared against: frontmatter `go_live: YYYY-MM-DD` or a `**Go-live:** YYYY-MM-DD` line.
|
|
136
|
+
`qa_ledger.py spec-check` then reports, per component, `ok` / `expires before go-live` /
|
|
137
|
+
`no EOL cited` / `no source cited` — ADVISORY, it never gates. It can see that a date
|
|
138
|
+
was CITED; it cannot see whether the citation is TRUE. That part is yours.
|
|
139
|
+
4. **Behavior.** Happy path first, then the DIRTY cases: provider/timeout failures,
|
|
116
140
|
retries and backoff, 4xx vs 5xx, concurrency, partial/terminal states, what must NOT
|
|
117
141
|
happen. A feature without its failure behavior is half-specified.
|
|
118
|
-
|
|
142
|
+
5. **Inviolable constraints (→ `CONSTITUTION.md`).** Domain + security + operation
|
|
119
143
|
rules that cannot be broken (money to the cent, numbering without gaps, never cross
|
|
120
144
|
environments/credentials, secrets never logged, auth/authz, data retention). Write/extend
|
|
121
145
|
`CONSTITUTION.md` (one invariant per line, CWE ref where it maps); these feed the
|
|
122
146
|
dev-loop severity gate and a breach is a BLOCKER. **An ADR may never contradict the
|
|
123
147
|
CONSTITUTION** — if a decision would, it's escalated, not recorded.
|
|
124
|
-
|
|
148
|
+
6. **Out of scope.** Explicit boundaries, with forward references ("X goes to a later
|
|
125
149
|
spec"). What you exclude is as important as what you include.
|
|
126
|
-
|
|
150
|
+
7. **Definition of Done + how we measure success.** Concrete, checkable acceptance criteria
|
|
127
151
|
(tests green, documented, metrics published, runbook) AND success metrics (p95,
|
|
128
152
|
cost ceiling, zero orphaned records). Each item must be verifiable, not a feeling.
|
|
129
|
-
|
|
153
|
+
8. **Dependencies.** Which other specs/systems/credentials this needs to exist first.
|
|
130
154
|
|
|
131
155
|
After each batch, reflect: "Decided: … / Still open: …". Move on only when the
|
|
132
156
|
current topic is closed.
|
|
@@ -146,7 +170,7 @@ have behavior, the scope has a boundary and the DoD is verifiable.") before Phas
|
|
|
146
170
|
## Phase B — Distill the artifacts
|
|
147
171
|
|
|
148
172
|
Only after convergence. Produce (and, if any new project-wide invariant surfaced in
|
|
149
|
-
step
|
|
173
|
+
step 5, append it to **`CONSTITUTION.md`** — never let an inviolable rule slip into an
|
|
150
174
|
ADR where it could later be "traded away"):
|
|
151
175
|
|
|
152
176
|
1. **One ADR per decision worth recording** at `docs/adr/ADR-NNN-<slug>.md`, format:
|
|
@@ -8406,6 +8406,10 @@ def cmd_dashboard(args):
|
|
|
8406
8406
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
8407
8407
|
if ledger.get("spec_drift"):
|
|
8408
8408
|
out["spec_drift"] = ledger["spec_drift"]
|
|
8409
|
+
# lifecycle (ADR-040): reused VERBATIM from the readiness payload above (one read, no
|
|
8410
|
+
# drift possible between the two surfaces), conditional for the same reason spec_drift is.
|
|
8411
|
+
if rd.get("lifecycle"):
|
|
8412
|
+
out["lifecycle"] = rd["lifecycle"]
|
|
8409
8413
|
# evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
|
|
8410
8414
|
# a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
|
|
8411
8415
|
# fast_path and spec_drift already follow).
|
|
@@ -9554,6 +9558,12 @@ def cmd_readiness(args):
|
|
|
9554
9558
|
"gates": _gate_rollup(ledger),
|
|
9555
9559
|
"by_repo": repos,
|
|
9556
9560
|
}
|
|
9561
|
+
# lifecycle (ADR-040): advisory, and CONDITIONAL like fast_path/spec_drift -- a project
|
|
9562
|
+
# that declares no lifecycle: block keeps the exact prior payload and the exact prior
|
|
9563
|
+
# text. Speaking only when it matters is the anti-ceremony rule applied to itself.
|
|
9564
|
+
_lc = _lifecycle_for(os.path.dirname(os.path.abspath(args.ledger)) or os.getcwd())
|
|
9565
|
+
if _lc["declared"]:
|
|
9566
|
+
out["lifecycle"] = _lc
|
|
9557
9567
|
if args.json:
|
|
9558
9568
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
9559
9569
|
return
|
|
@@ -9634,6 +9644,8 @@ def cmd_readiness(args):
|
|
|
9634
9644
|
f"rising for {STALL_WINDOW} cycles: iterating more is not getting "
|
|
9635
9645
|
f"closer. Likely a design/SPEC problem — go back to the ADR / "
|
|
9636
9646
|
f"re-plan with the human (advisory)")
|
|
9647
|
+
if _lc["declared"]:
|
|
9648
|
+
print(" · " + _lifecycle_summary(_lc))
|
|
9637
9649
|
# rubrica (1.23.0): el ultimo grade por repo, siempre visible — guess
|
|
9638
9650
|
# estructurado que aconseja; si esta gateado ya bloqueo por el ledger
|
|
9639
9651
|
for rname, rnode in ledger["repos"].items():
|
|
@@ -10986,6 +10998,225 @@ _SC_SETEXT = re.compile(r"^(=+|-{2,})\s*$")
|
|
|
10986
10998
|
_SC_CHECKBOX = re.compile(r"^\[[ xX]\]\s*")
|
|
10987
10999
|
|
|
10988
11000
|
|
|
11001
|
+
# --------------------------------------------------------------------------- #
|
|
11002
|
+
# lifecycle (ADR-040: stack end-of-support vs go-live -- advisory, NEVER gates)
|
|
11003
|
+
# --------------------------------------------------------------------------- #
|
|
11004
|
+
# The method interrogates WHAT and WHY and has always treated the stack as a given
|
|
11005
|
+
# ("we use X"). A major line is not a decision: a minor line has an END-OF-SUPPORT
|
|
11006
|
+
# DATE, and a date that falls before the declared go-live is a forced upgrade nobody
|
|
11007
|
+
# planned. This dimension reads what the ADR CITES and compares it against the SPEC's
|
|
11008
|
+
# declared go-live. It cannot verify that the cited source tells the truth -- only that
|
|
11009
|
+
# a date and a source were cited at all. Advisory by construction, like spec-drift
|
|
11010
|
+
# (ADR-005): it never gates, never caps readiness, and never changes an exit code.
|
|
11011
|
+
_LC_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
11012
|
+
|
|
11013
|
+
|
|
11014
|
+
def _lc_valid_date(s):
|
|
11015
|
+
"""A cited date is real only if it is BOTH shaped YYYY-MM-DD and a calendar date:
|
|
11016
|
+
the format regex alone accepts 2025-13-40 (1.94.0 review), which then string-compares
|
|
11017
|
+
as if it were a real EOL. fromisoformat rejects the impossible month/day."""
|
|
11018
|
+
if not s or not _LC_DATE.match(s):
|
|
11019
|
+
return False
|
|
11020
|
+
try:
|
|
11021
|
+
datetime.strptime(s, "%Y-%m-%d")
|
|
11022
|
+
return True
|
|
11023
|
+
except ValueError:
|
|
11024
|
+
return False
|
|
11025
|
+
_LC_KEY = re.compile(r"^lifecycle\s*:\s*(.*)$")
|
|
11026
|
+
_LC_GOLIVE_FM = re.compile(r"^go[-_ ]?live\s*:\s*(.+?)\s*$", re.I)
|
|
11027
|
+
_LC_GOLIVE_LINE = re.compile(r"^\s*\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
|
|
11028
|
+
r"(\d{4}-\d{2}-\d{2})\b", re.I)
|
|
11029
|
+
_LC_FIELDS = ("component", "version", "eol", "source", "checked")
|
|
11030
|
+
|
|
11031
|
+
|
|
11032
|
+
def _lc_frontmatter(lines):
|
|
11033
|
+
"""The lines INSIDE a leading `---` frontmatter block, or None when there is none.
|
|
11034
|
+
An unterminated fence is not data: a half-written block reads as absent."""
|
|
11035
|
+
if not lines or lines[0].strip() != "---":
|
|
11036
|
+
return None
|
|
11037
|
+
body = []
|
|
11038
|
+
for ln in lines[1:]:
|
|
11039
|
+
if ln.strip() == "---":
|
|
11040
|
+
return body
|
|
11041
|
+
body.append(ln)
|
|
11042
|
+
return None
|
|
11043
|
+
|
|
11044
|
+
|
|
11045
|
+
def _lc_parse(lines):
|
|
11046
|
+
"""The `lifecycle:` list-of-dicts of a markdown file, parsed as a YAML SUBSET --
|
|
11047
|
+
stdlib only, the kit ships no PyYAML. Returns None when the key is absent (nothing to
|
|
11048
|
+
measure) and [] when the key is there but nothing under it parses (a NAMED absence:
|
|
11049
|
+
the caller reports UNMEASURED with a reason, never a silent pass)."""
|
|
11050
|
+
fm = _lc_frontmatter(lines)
|
|
11051
|
+
if fm is None:
|
|
11052
|
+
return None
|
|
11053
|
+
entries, in_lc, cur = None, False, None
|
|
11054
|
+
for ln in fm:
|
|
11055
|
+
s = ln.strip()
|
|
11056
|
+
if not in_lc:
|
|
11057
|
+
m = _LC_KEY.match(ln)
|
|
11058
|
+
if m:
|
|
11059
|
+
entries = []
|
|
11060
|
+
# only the block form is supported; an inline `lifecycle: [...]` parses to
|
|
11061
|
+
# nothing and reads as unparseable, never as "this ADR fixes no component"
|
|
11062
|
+
in_lc = not m.group(1).strip()
|
|
11063
|
+
continue
|
|
11064
|
+
if not s or s.startswith("#"):
|
|
11065
|
+
continue
|
|
11066
|
+
if not ln[:1].isspace(): # back to column 0 -> the key ended
|
|
11067
|
+
in_lc = False
|
|
11068
|
+
continue
|
|
11069
|
+
if s.startswith("- "):
|
|
11070
|
+
cur = {}
|
|
11071
|
+
entries.append(cur)
|
|
11072
|
+
s = s[2:].strip()
|
|
11073
|
+
if not s:
|
|
11074
|
+
continue
|
|
11075
|
+
if cur is None:
|
|
11076
|
+
continue
|
|
11077
|
+
k, sep, v = s.partition(":")
|
|
11078
|
+
if not sep:
|
|
11079
|
+
continue
|
|
11080
|
+
k = k.strip().lower()
|
|
11081
|
+
if k in _LC_FIELDS:
|
|
11082
|
+
cur[k] = v.strip().strip("\x27\x22")
|
|
11083
|
+
if entries is not None:
|
|
11084
|
+
entries = [e for e in entries if e]
|
|
11085
|
+
return entries
|
|
11086
|
+
|
|
11087
|
+
|
|
11088
|
+
def _lc_go_live(text):
|
|
11089
|
+
"""The declared go-live of a SPEC: frontmatter `go_live: YYYY-MM-DD`, or a
|
|
11090
|
+
`**Go-live:** YYYY-MM-DD` line anywhere in the body. None = not declared."""
|
|
11091
|
+
lines = [ln.rstrip("\r") for ln in (text or "").split("\n")]
|
|
11092
|
+
fm = _lc_frontmatter(lines)
|
|
11093
|
+
if fm:
|
|
11094
|
+
for ln in fm:
|
|
11095
|
+
if ln[:1].isspace():
|
|
11096
|
+
continue
|
|
11097
|
+
m = _LC_GOLIVE_FM.match(ln.strip())
|
|
11098
|
+
if m:
|
|
11099
|
+
v = m.group(1).strip().strip("\x27\x22")
|
|
11100
|
+
if _lc_valid_date(v):
|
|
11101
|
+
return v
|
|
11102
|
+
for ln in lines:
|
|
11103
|
+
m = _LC_GOLIVE_LINE.match(ln)
|
|
11104
|
+
if m and _lc_valid_date(m.group(1)):
|
|
11105
|
+
return m.group(1)
|
|
11106
|
+
return None
|
|
11107
|
+
|
|
11108
|
+
|
|
11109
|
+
def _lc_short(path):
|
|
11110
|
+
"""The ADR dir as the reader would type it: relative to the cwd when that is shorter,
|
|
11111
|
+
absolute otherwise (a different drive on Windows has no relative form)."""
|
|
11112
|
+
if not path:
|
|
11113
|
+
return "docs/adr"
|
|
11114
|
+
try:
|
|
11115
|
+
rel = os.path.relpath(path)
|
|
11116
|
+
except ValueError:
|
|
11117
|
+
return path
|
|
11118
|
+
return rel if len(rel) < len(path) else path
|
|
11119
|
+
|
|
11120
|
+
|
|
11121
|
+
def _lifecycle_report(adr_dir, spec_text):
|
|
11122
|
+
"""Per-component verdicts for every `lifecycle:` entry under `adr_dir`, compared
|
|
11123
|
+
against the go-live declared in `spec_text`. Whole-dimension UNMEASURED -- with the
|
|
11124
|
+
reason spelled out, never silence -- when no ADR carries the block, when the block is
|
|
11125
|
+
unparseable, or when no go-live is declared."""
|
|
11126
|
+
go_live = _lc_go_live(spec_text)
|
|
11127
|
+
comps, unparsed, names = [], [], []
|
|
11128
|
+
if adr_dir and os.path.isdir(adr_dir):
|
|
11129
|
+
names = sorted(f for f in os.listdir(adr_dir) if f.lower().endswith(".md"))
|
|
11130
|
+
for fn in names:
|
|
11131
|
+
try:
|
|
11132
|
+
with open(os.path.join(adr_dir, fn), "r", encoding="utf-8",
|
|
11133
|
+
errors="replace") as fh:
|
|
11134
|
+
lines = [ln.rstrip("\r") for ln in fh.read().split("\n")]
|
|
11135
|
+
except OSError:
|
|
11136
|
+
continue
|
|
11137
|
+
ent = _lc_parse(lines)
|
|
11138
|
+
if ent is None:
|
|
11139
|
+
continue
|
|
11140
|
+
if not ent:
|
|
11141
|
+
unparsed.append(fn)
|
|
11142
|
+
continue
|
|
11143
|
+
for e in ent:
|
|
11144
|
+
comps.append({"adr": fn, "component": e.get("component") or "(unnamed)",
|
|
11145
|
+
"version": e.get("version"), "eol": e.get("eol"),
|
|
11146
|
+
"source": e.get("source"), "checked": e.get("checked")})
|
|
11147
|
+
out = {"status": "UNMEASURED", "reason": None, "go_live": go_live,
|
|
11148
|
+
"adr_dir": adr_dir, "declared": bool(comps or unparsed),
|
|
11149
|
+
"components": comps, "n_components": len(comps), "n_expiring": 0,
|
|
11150
|
+
"unparsed_adrs": unparsed}
|
|
11151
|
+
if not comps:
|
|
11152
|
+
out["reason"] = ("lifecycle: block declared but unparseable in: "
|
|
11153
|
+
+ ", ".join(unparsed) if unparsed else
|
|
11154
|
+
"no ADR under %s carries a lifecycle: block"
|
|
11155
|
+
% _lc_short(adr_dir))
|
|
11156
|
+
return out
|
|
11157
|
+
if not go_live:
|
|
11158
|
+
for c in comps:
|
|
11159
|
+
c["status"], c["detail"] = "unmeasured", "no go-live declared"
|
|
11160
|
+
out["reason"] = ("no go-live declared in the SPEC (frontmatter 'go_live: "
|
|
11161
|
+
"YYYY-MM-DD' or a '**Go-live:** YYYY-MM-DD' line)")
|
|
11162
|
+
return out
|
|
11163
|
+
for c in comps:
|
|
11164
|
+
eol = (c.get("eol") or "").strip()
|
|
11165
|
+
cited = (c.get("source") or "").strip()
|
|
11166
|
+
if not eol or eol.lower() == "unknown" or not _lc_valid_date(eol):
|
|
11167
|
+
c["status"] = "no EOL cited"
|
|
11168
|
+
c["detail"] = ("declared unknown" if eol.lower() == "unknown"
|
|
11169
|
+
else ("not a YYYY-MM-DD date: %s" % eol if eol else "absent"))
|
|
11170
|
+
elif eol < go_live:
|
|
11171
|
+
# the sharper fact wins the label: a date that already expires is reported as
|
|
11172
|
+
# expiring even when uncited, and the missing source stays visible in the record
|
|
11173
|
+
c["status"] = "expires before go-live"
|
|
11174
|
+
c["detail"] = "%s < %s" % (eol, go_live)
|
|
11175
|
+
out["n_expiring"] += 1
|
|
11176
|
+
elif not cited:
|
|
11177
|
+
c["status"] = "no source cited"
|
|
11178
|
+
c["detail"] = "eol %s is asserted, not cited" % eol
|
|
11179
|
+
else:
|
|
11180
|
+
c["status"] = "ok"
|
|
11181
|
+
c["detail"] = "eol %s >= go-live %s" % (eol, go_live)
|
|
11182
|
+
out["status"] = "measured"
|
|
11183
|
+
return out
|
|
11184
|
+
|
|
11185
|
+
|
|
11186
|
+
def _lifecycle_summary(lc):
|
|
11187
|
+
"""The one advisory line, shared by spec-check and readiness so the two surfaces
|
|
11188
|
+
cannot drift apart."""
|
|
11189
|
+
if lc["status"] != "measured":
|
|
11190
|
+
return "lifecycle: UNMEASURED - %s" % lc["reason"]
|
|
11191
|
+
return ("lifecycle: %d component(s), %d expire before go-live %s (advisory)"
|
|
11192
|
+
% (lc["n_components"], lc["n_expiring"], lc["go_live"]))
|
|
11193
|
+
|
|
11194
|
+
|
|
11195
|
+
def _lifecycle_root(*paths):
|
|
11196
|
+
"""The project root a lifecycle read hangs off: the directory of the first path
|
|
11197
|
+
given, else the cwd."""
|
|
11198
|
+
for p in paths:
|
|
11199
|
+
if p:
|
|
11200
|
+
return os.path.dirname(os.path.abspath(p)) or os.getcwd()
|
|
11201
|
+
return os.getcwd()
|
|
11202
|
+
|
|
11203
|
+
|
|
11204
|
+
def _lifecycle_for(root, adr_dir=None, spec_text=None, fallback=True):
|
|
11205
|
+
"""Read the dimension for a project root: `docs/adr` unless overridden, and the
|
|
11206
|
+
go-live from the text already in hand. `fallback` reads `<root>/SPEC.md` when no
|
|
11207
|
+
text was supplied -- it is OFF when the caller named the spec files itself, so an
|
|
11208
|
+
explicit `--spec` is never silently topped up from a file nobody asked for."""
|
|
11209
|
+
if fallback and not _lc_go_live(spec_text):
|
|
11210
|
+
sp = os.path.join(root, "SPEC.md")
|
|
11211
|
+
if os.path.isfile(sp):
|
|
11212
|
+
try:
|
|
11213
|
+
with open(sp, "r", encoding="utf-8", errors="replace") as fh:
|
|
11214
|
+
spec_text = (spec_text or "") + "\n" + fh.read()
|
|
11215
|
+
except OSError:
|
|
11216
|
+
pass
|
|
11217
|
+
return _lifecycle_report(adr_dir or os.path.join(root, "docs", "adr"), spec_text)
|
|
11218
|
+
|
|
11219
|
+
|
|
10989
11220
|
def _spec_check_text(text):
|
|
10990
11221
|
lines = text.split("\n")
|
|
10991
11222
|
n = len(lines)
|
|
@@ -11280,6 +11511,11 @@ def cmd_spec_check(args):
|
|
|
11280
11511
|
m = (_spec_check_text(text) if text.strip()
|
|
11281
11512
|
else {"blockers": [], "untestable": [], "stack_hits": [],
|
|
11282
11513
|
"non_ears": 0, "n_criteria": 0})
|
|
11514
|
+
# lifecycle (ADR-040): read-only and advisory -- it never touches `fail` below.
|
|
11515
|
+
lc = _lifecycle_for(_lifecycle_root(args.spec[0] if args.spec else None,
|
|
11516
|
+
args.acceptance),
|
|
11517
|
+
getattr(args, "adr_dir", None), text,
|
|
11518
|
+
fallback=not args.spec)
|
|
11283
11519
|
structural = len(m["blockers"]) + len(acc_block) # estructura = FACT -> bloquea
|
|
11284
11520
|
soft_find = len(m["untestable"]) + len(m["stack_hits"]) + len(acc_adv)
|
|
11285
11521
|
fail = structural > 0 or (args.strict and soft_find > 0)
|
|
@@ -11288,7 +11524,7 @@ def cmd_spec_check(args):
|
|
|
11288
11524
|
if args.json:
|
|
11289
11525
|
print(json.dumps({"verdict": verdict, "advisory": structural == 0,
|
|
11290
11526
|
"acceptance_blockers": acc_block,
|
|
11291
|
-
"acceptance_advisory": acc_adv, **m},
|
|
11527
|
+
"acceptance_advisory": acc_adv, "lifecycle": lc, **m},
|
|
11292
11528
|
indent=2, ensure_ascii=False))
|
|
11293
11529
|
sys.exit(1 if fail else 0)
|
|
11294
11530
|
|
|
@@ -11309,6 +11545,11 @@ def cmd_spec_check(args):
|
|
|
11309
11545
|
print(f" ~ stack named in criteria ({len(m['stack_hits'])}): {', '.join(terms[:6])} — the 'how' belongs in the ADR, not the SPEC")
|
|
11310
11546
|
if m["non_ears"] and m["n_criteria"]:
|
|
11311
11547
|
print(f" · advisory: {m['non_ears']}/{m['n_criteria']} criteria do not follow the EARS pattern (When/If/While … shall)")
|
|
11548
|
+
print(" · " + _lifecycle_summary(lc))
|
|
11549
|
+
for c in [x for x in lc["components"] if x.get("status") not in ("ok", None)][:5]:
|
|
11550
|
+
print(" %s %s %s - %s (%s)"
|
|
11551
|
+
% ("!" if c["status"] == "expires before go-live" else "~",
|
|
11552
|
+
c["component"], c.get("version") or "?", c["status"], c["detail"]))
|
|
11312
11553
|
print(" i consistency: INFERENTIAL (an uncorrelated checker), not this lint · "
|
|
11313
11554
|
"structure = FACT (blocks) · prose = advisory (--strict to gate)")
|
|
11314
11555
|
if verdict == "OK":
|
|
@@ -12529,6 +12770,9 @@ def build_parser():
|
|
|
12529
12770
|
help="validate ACCEPTANCE traceability (AC-n IDs): missing "
|
|
12530
12771
|
"file / zero criteria / zero traceable / duplicate "
|
|
12531
12772
|
"IDs block as structural FACTS")
|
|
12773
|
+
psc.add_argument("--adr-dir", default=None,
|
|
12774
|
+
help="ADRs whose lifecycle: blocks are read (ADR-040); default "
|
|
12775
|
+
"docs/adr beside the SPEC")
|
|
12532
12776
|
psc.add_argument("--strict", action="store_true",
|
|
12533
12777
|
help="also fail (exit 1) on soft findings: untestable criteria / stack named")
|
|
12534
12778
|
psc.add_argument("--json", action="store_true")
|
|
@@ -137,25 +137,49 @@ answer.
|
|
|
137
137
|
I deduce these core entities: … do they work for you, or is one missing?")
|
|
138
138
|
3. **Operation / API surface.** Propose the endpoints/operations and their
|
|
139
139
|
contracts (idempotency, status codes).
|
|
140
|
-
4. **
|
|
140
|
+
4. **Stack and lifecycle (MANDATORY — before any stack/architecture decision is
|
|
141
|
+
recorded).** The stack is not a given ("we use X"): it is a decision with an EXPIRY
|
|
142
|
+
DATE. A major version is a family; support is granted to a MINOR line, for a window,
|
|
143
|
+
by an upstream nobody in the room controls. Ask these one at a time, each with your
|
|
144
|
+
recommended answer, and **FETCH the dates from the official source AS YOU ASK — never
|
|
145
|
+
answer from memory**; record the URL and the day you checked:
|
|
146
|
+
(a) the EXACT version of every runtime/framework/store (JDK, web framework, ORM, DB,
|
|
147
|
+
Node, bundler, broker, cache) and its OSS/LTS end-of-support date, cited;
|
|
148
|
+
(b) support window vs the declared go-live AND the expected operating life — does the
|
|
149
|
+
line stay patched for the whole operation? If not, move it up BEFORE building: a major
|
|
150
|
+
upgrade days before the milestone is the most expensive one there is;
|
|
151
|
+
(c) major dependencies and the upgrade policy — who approves one, and when it is
|
|
152
|
+
scheduled (aligned with the dev-loop's "zero new dependencies without approval");
|
|
153
|
+
(d) development and observability tools the operator wants from day one (consoles, APM,
|
|
154
|
+
admin UIs) — they CONSTRAIN versions, so ask early or they force the upgrade;
|
|
155
|
+
(e) compatibility with reused legacy modules — the minimum version they support.
|
|
156
|
+
The answers distil into the stack ADR with the dates INSIDE it, as the machine-readable
|
|
157
|
+
`lifecycle:` frontmatter block (`component` / `version` / `eol` / `source` / `checked`,
|
|
158
|
+
ISO dates; `eol: unknown` is allowed and reads as a NAMED absence, never a pass) — see
|
|
159
|
+
`templates/docs/adr/ADR-stack-template.md`. The SPEC declares the milestone it is
|
|
160
|
+
compared against: frontmatter `go_live: YYYY-MM-DD` or a `**Go-live:** YYYY-MM-DD` line.
|
|
161
|
+
`qa_ledger.py spec-check` then reports, per component, `ok` / `expires before go-live` /
|
|
162
|
+
`no EOL cited` / `no source cited` — ADVISORY, it never gates. It can see that a date
|
|
163
|
+
was CITED; it cannot see whether the citation is TRUE. That part is yours.
|
|
164
|
+
5. **Big decisions (→ ADR).** Propose 2–3 architecture options with trade-offs and
|
|
141
165
|
a recommended default: persistence, protocol, idempotency, sync/async, multi-tenancy.
|
|
142
|
-
|
|
166
|
+
6. **Behavior and dirty cases.** Happy path, then failures, retries, partial states,
|
|
143
167
|
concurrency, what must NOT happen.
|
|
144
|
-
|
|
168
|
+
7. **Inviolable constraints (→ `CONSTITUTION.md`).** Domain + security + operation
|
|
145
169
|
rules that can't be broken (money to the cent, no numbering gaps, never cross
|
|
146
170
|
environments, secrets never logged, auth). Write/extend `CONSTITUTION.md` with these —
|
|
147
171
|
one invariant per line, with a CWE reference where it maps. They feed the severity gate
|
|
148
172
|
downstream, and a breach is a BLOCKER, never a trade-off.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
8. **Out of scope.** Explicit boundaries with forward references.
|
|
174
|
+
9. **Acceptance / Definition of Done.** Concrete, checkable criteria + success metrics.
|
|
175
|
+
10. **Quality bar (→ config, kit 1.17.0).** "What level of quality is ENOUGH here, and which
|
|
152
176
|
dimensions are negotiable (coverage, perf, security)?" Propose thresholds fit for
|
|
153
177
|
the risk profile (a payments core is not an internal dashboard). What the human
|
|
154
178
|
declares goes into `uscha.config.json` (`defaults.coverage_threshold`,
|
|
155
179
|
`defaults.readiness_caps`, `defaults.simplicity`) — a declared threshold reads as
|
|
156
180
|
**requerimiento (config)** in the engine's output; an undeclared one stays a kit
|
|
157
181
|
default (opinion) and is labeled as such. Declaring is committing the config.
|
|
158
|
-
|
|
182
|
+
11. **Residual risks and dependencies.** What's uncertain, what must exist first.
|
|
159
183
|
For each HIGH-uncertainty risk, ask (kit 1.19.0, Tip 21 'Prototype to Learn'):
|
|
160
184
|
"Does it warrant a time-boxed spike before freezing the SPEC?" A spike runs on a
|
|
161
185
|
`spike/*` branch and its ONLY legitimate output is an **ADR with lessons**
|
|
@@ -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.
|
|
4
|
+
"version": "1.94.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": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.94.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.94.0
|
|
@@ -112,21 +112,45 @@ it's already fully answered. Keep a running list of OPEN GAPS and resolved decis
|
|
|
112
112
|
2. **Implicit decisions.** Surface the choices the request assumed: sync vs async,
|
|
113
113
|
storage, protocol, idempotency, transactional boundaries, who owns state. For each,
|
|
114
114
|
force an explicit decision and at least one considered alternative.
|
|
115
|
-
3. **
|
|
115
|
+
3. **Stack and lifecycle (MANDATORY — before any stack/architecture decision is
|
|
116
|
+
recorded).** The stack is not a given ("we use X"): it is a decision with an EXPIRY
|
|
117
|
+
DATE. A major version is a family; support is granted to a MINOR line, for a window,
|
|
118
|
+
by an upstream nobody in the room controls. Ask these one at a time, each with your
|
|
119
|
+
recommended answer, and **FETCH the dates from the official source AS YOU ASK — never
|
|
120
|
+
answer from memory**; record the URL and the day you checked:
|
|
121
|
+
(a) the EXACT version of every runtime/framework/store (JDK, web framework, ORM, DB,
|
|
122
|
+
Node, bundler, broker, cache) and its OSS/LTS end-of-support date, cited;
|
|
123
|
+
(b) support window vs the declared go-live AND the expected operating life — does the
|
|
124
|
+
line stay patched for the whole operation? If not, move it up BEFORE building: a major
|
|
125
|
+
upgrade days before the milestone is the most expensive one there is;
|
|
126
|
+
(c) major dependencies and the upgrade policy — who approves one, and when it is
|
|
127
|
+
scheduled (aligned with the dev-loop's "zero new dependencies without approval");
|
|
128
|
+
(d) development and observability tools the operator wants from day one (consoles, APM,
|
|
129
|
+
admin UIs) — they CONSTRAIN versions, so ask early or they force the upgrade;
|
|
130
|
+
(e) compatibility with reused legacy modules — the minimum version they support.
|
|
131
|
+
The answers distil into the stack ADR with the dates INSIDE it, as the machine-readable
|
|
132
|
+
`lifecycle:` frontmatter block (`component` / `version` / `eol` / `source` / `checked`,
|
|
133
|
+
ISO dates; `eol: unknown` is allowed and reads as a NAMED absence, never a pass) — see
|
|
134
|
+
`templates/docs/adr/ADR-stack-template.md`. The SPEC declares the milestone it is
|
|
135
|
+
compared against: frontmatter `go_live: YYYY-MM-DD` or a `**Go-live:** YYYY-MM-DD` line.
|
|
136
|
+
`qa_ledger.py spec-check` then reports, per component, `ok` / `expires before go-live` /
|
|
137
|
+
`no EOL cited` / `no source cited` — ADVISORY, it never gates. It can see that a date
|
|
138
|
+
was CITED; it cannot see whether the citation is TRUE. That part is yours.
|
|
139
|
+
4. **Behavior.** Happy path first, then the DIRTY cases: provider/timeout failures,
|
|
116
140
|
retries and backoff, 4xx vs 5xx, concurrency, partial/terminal states, what must NOT
|
|
117
141
|
happen. A feature without its failure behavior is half-specified.
|
|
118
|
-
|
|
142
|
+
5. **Inviolable constraints (→ `CONSTITUTION.md`).** Domain + security + operation
|
|
119
143
|
rules that cannot be broken (money to the cent, numbering without gaps, never cross
|
|
120
144
|
environments/credentials, secrets never logged, auth/authz, data retention). Write/extend
|
|
121
145
|
`CONSTITUTION.md` (one invariant per line, CWE ref where it maps); these feed the
|
|
122
146
|
dev-loop severity gate and a breach is a BLOCKER. **An ADR may never contradict the
|
|
123
147
|
CONSTITUTION** — if a decision would, it's escalated, not recorded.
|
|
124
|
-
|
|
148
|
+
6. **Out of scope.** Explicit boundaries, with forward references ("X goes to a later
|
|
125
149
|
spec"). What you exclude is as important as what you include.
|
|
126
|
-
|
|
150
|
+
7. **Definition of Done + how we measure success.** Concrete, checkable acceptance criteria
|
|
127
151
|
(tests green, documented, metrics published, runbook) AND success metrics (p95,
|
|
128
152
|
cost ceiling, zero orphaned records). Each item must be verifiable, not a feeling.
|
|
129
|
-
|
|
153
|
+
8. **Dependencies.** Which other specs/systems/credentials this needs to exist first.
|
|
130
154
|
|
|
131
155
|
After each batch, reflect: "Decided: … / Still open: …". Move on only when the
|
|
132
156
|
current topic is closed.
|
|
@@ -146,7 +170,7 @@ have behavior, the scope has a boundary and the DoD is verifiable.") before Phas
|
|
|
146
170
|
## Phase B — Distill the artifacts
|
|
147
171
|
|
|
148
172
|
Only after convergence. Produce (and, if any new project-wide invariant surfaced in
|
|
149
|
-
step
|
|
173
|
+
step 5, append it to **`CONSTITUTION.md`** — never let an inviolable rule slip into an
|
|
150
174
|
ADR where it could later be "traded away"):
|
|
151
175
|
|
|
152
176
|
1. **One ADR per decision worth recording** at `docs/adr/ADR-NNN-<slug>.md`, format:
|
|
@@ -8406,6 +8406,10 @@ def cmd_dashboard(args):
|
|
|
8406
8406
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
8407
8407
|
if ledger.get("spec_drift"):
|
|
8408
8408
|
out["spec_drift"] = ledger["spec_drift"]
|
|
8409
|
+
# lifecycle (ADR-040): reused VERBATIM from the readiness payload above (one read, no
|
|
8410
|
+
# drift possible between the two surfaces), conditional for the same reason spec_drift is.
|
|
8411
|
+
if rd.get("lifecycle"):
|
|
8412
|
+
out["lifecycle"] = rd["lifecycle"]
|
|
8409
8413
|
# evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
|
|
8410
8414
|
# a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
|
|
8411
8415
|
# fast_path and spec_drift already follow).
|
|
@@ -9554,6 +9558,12 @@ def cmd_readiness(args):
|
|
|
9554
9558
|
"gates": _gate_rollup(ledger),
|
|
9555
9559
|
"by_repo": repos,
|
|
9556
9560
|
}
|
|
9561
|
+
# lifecycle (ADR-040): advisory, and CONDITIONAL like fast_path/spec_drift -- a project
|
|
9562
|
+
# that declares no lifecycle: block keeps the exact prior payload and the exact prior
|
|
9563
|
+
# text. Speaking only when it matters is the anti-ceremony rule applied to itself.
|
|
9564
|
+
_lc = _lifecycle_for(os.path.dirname(os.path.abspath(args.ledger)) or os.getcwd())
|
|
9565
|
+
if _lc["declared"]:
|
|
9566
|
+
out["lifecycle"] = _lc
|
|
9557
9567
|
if args.json:
|
|
9558
9568
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
9559
9569
|
return
|
|
@@ -9634,6 +9644,8 @@ def cmd_readiness(args):
|
|
|
9634
9644
|
f"rising for {STALL_WINDOW} cycles: iterating more is not getting "
|
|
9635
9645
|
f"closer. Likely a design/SPEC problem — go back to the ADR / "
|
|
9636
9646
|
f"re-plan with the human (advisory)")
|
|
9647
|
+
if _lc["declared"]:
|
|
9648
|
+
print(" · " + _lifecycle_summary(_lc))
|
|
9637
9649
|
# rubrica (1.23.0): el ultimo grade por repo, siempre visible — guess
|
|
9638
9650
|
# estructurado que aconseja; si esta gateado ya bloqueo por el ledger
|
|
9639
9651
|
for rname, rnode in ledger["repos"].items():
|
|
@@ -10986,6 +10998,225 @@ _SC_SETEXT = re.compile(r"^(=+|-{2,})\s*$")
|
|
|
10986
10998
|
_SC_CHECKBOX = re.compile(r"^\[[ xX]\]\s*")
|
|
10987
10999
|
|
|
10988
11000
|
|
|
11001
|
+
# --------------------------------------------------------------------------- #
|
|
11002
|
+
# lifecycle (ADR-040: stack end-of-support vs go-live -- advisory, NEVER gates)
|
|
11003
|
+
# --------------------------------------------------------------------------- #
|
|
11004
|
+
# The method interrogates WHAT and WHY and has always treated the stack as a given
|
|
11005
|
+
# ("we use X"). A major line is not a decision: a minor line has an END-OF-SUPPORT
|
|
11006
|
+
# DATE, and a date that falls before the declared go-live is a forced upgrade nobody
|
|
11007
|
+
# planned. This dimension reads what the ADR CITES and compares it against the SPEC's
|
|
11008
|
+
# declared go-live. It cannot verify that the cited source tells the truth -- only that
|
|
11009
|
+
# a date and a source were cited at all. Advisory by construction, like spec-drift
|
|
11010
|
+
# (ADR-005): it never gates, never caps readiness, and never changes an exit code.
|
|
11011
|
+
_LC_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
11012
|
+
|
|
11013
|
+
|
|
11014
|
+
def _lc_valid_date(s):
|
|
11015
|
+
"""A cited date is real only if it is BOTH shaped YYYY-MM-DD and a calendar date:
|
|
11016
|
+
the format regex alone accepts 2025-13-40 (1.94.0 review), which then string-compares
|
|
11017
|
+
as if it were a real EOL. fromisoformat rejects the impossible month/day."""
|
|
11018
|
+
if not s or not _LC_DATE.match(s):
|
|
11019
|
+
return False
|
|
11020
|
+
try:
|
|
11021
|
+
datetime.strptime(s, "%Y-%m-%d")
|
|
11022
|
+
return True
|
|
11023
|
+
except ValueError:
|
|
11024
|
+
return False
|
|
11025
|
+
_LC_KEY = re.compile(r"^lifecycle\s*:\s*(.*)$")
|
|
11026
|
+
_LC_GOLIVE_FM = re.compile(r"^go[-_ ]?live\s*:\s*(.+?)\s*$", re.I)
|
|
11027
|
+
_LC_GOLIVE_LINE = re.compile(r"^\s*\*\*\s*go[-_ ]?live\s*:?\s*\*\*\s*:?\s*"
|
|
11028
|
+
r"(\d{4}-\d{2}-\d{2})\b", re.I)
|
|
11029
|
+
_LC_FIELDS = ("component", "version", "eol", "source", "checked")
|
|
11030
|
+
|
|
11031
|
+
|
|
11032
|
+
def _lc_frontmatter(lines):
|
|
11033
|
+
"""The lines INSIDE a leading `---` frontmatter block, or None when there is none.
|
|
11034
|
+
An unterminated fence is not data: a half-written block reads as absent."""
|
|
11035
|
+
if not lines or lines[0].strip() != "---":
|
|
11036
|
+
return None
|
|
11037
|
+
body = []
|
|
11038
|
+
for ln in lines[1:]:
|
|
11039
|
+
if ln.strip() == "---":
|
|
11040
|
+
return body
|
|
11041
|
+
body.append(ln)
|
|
11042
|
+
return None
|
|
11043
|
+
|
|
11044
|
+
|
|
11045
|
+
def _lc_parse(lines):
|
|
11046
|
+
"""The `lifecycle:` list-of-dicts of a markdown file, parsed as a YAML SUBSET --
|
|
11047
|
+
stdlib only, the kit ships no PyYAML. Returns None when the key is absent (nothing to
|
|
11048
|
+
measure) and [] when the key is there but nothing under it parses (a NAMED absence:
|
|
11049
|
+
the caller reports UNMEASURED with a reason, never a silent pass)."""
|
|
11050
|
+
fm = _lc_frontmatter(lines)
|
|
11051
|
+
if fm is None:
|
|
11052
|
+
return None
|
|
11053
|
+
entries, in_lc, cur = None, False, None
|
|
11054
|
+
for ln in fm:
|
|
11055
|
+
s = ln.strip()
|
|
11056
|
+
if not in_lc:
|
|
11057
|
+
m = _LC_KEY.match(ln)
|
|
11058
|
+
if m:
|
|
11059
|
+
entries = []
|
|
11060
|
+
# only the block form is supported; an inline `lifecycle: [...]` parses to
|
|
11061
|
+
# nothing and reads as unparseable, never as "this ADR fixes no component"
|
|
11062
|
+
in_lc = not m.group(1).strip()
|
|
11063
|
+
continue
|
|
11064
|
+
if not s or s.startswith("#"):
|
|
11065
|
+
continue
|
|
11066
|
+
if not ln[:1].isspace(): # back to column 0 -> the key ended
|
|
11067
|
+
in_lc = False
|
|
11068
|
+
continue
|
|
11069
|
+
if s.startswith("- "):
|
|
11070
|
+
cur = {}
|
|
11071
|
+
entries.append(cur)
|
|
11072
|
+
s = s[2:].strip()
|
|
11073
|
+
if not s:
|
|
11074
|
+
continue
|
|
11075
|
+
if cur is None:
|
|
11076
|
+
continue
|
|
11077
|
+
k, sep, v = s.partition(":")
|
|
11078
|
+
if not sep:
|
|
11079
|
+
continue
|
|
11080
|
+
k = k.strip().lower()
|
|
11081
|
+
if k in _LC_FIELDS:
|
|
11082
|
+
cur[k] = v.strip().strip("\x27\x22")
|
|
11083
|
+
if entries is not None:
|
|
11084
|
+
entries = [e for e in entries if e]
|
|
11085
|
+
return entries
|
|
11086
|
+
|
|
11087
|
+
|
|
11088
|
+
def _lc_go_live(text):
|
|
11089
|
+
"""The declared go-live of a SPEC: frontmatter `go_live: YYYY-MM-DD`, or a
|
|
11090
|
+
`**Go-live:** YYYY-MM-DD` line anywhere in the body. None = not declared."""
|
|
11091
|
+
lines = [ln.rstrip("\r") for ln in (text or "").split("\n")]
|
|
11092
|
+
fm = _lc_frontmatter(lines)
|
|
11093
|
+
if fm:
|
|
11094
|
+
for ln in fm:
|
|
11095
|
+
if ln[:1].isspace():
|
|
11096
|
+
continue
|
|
11097
|
+
m = _LC_GOLIVE_FM.match(ln.strip())
|
|
11098
|
+
if m:
|
|
11099
|
+
v = m.group(1).strip().strip("\x27\x22")
|
|
11100
|
+
if _lc_valid_date(v):
|
|
11101
|
+
return v
|
|
11102
|
+
for ln in lines:
|
|
11103
|
+
m = _LC_GOLIVE_LINE.match(ln)
|
|
11104
|
+
if m and _lc_valid_date(m.group(1)):
|
|
11105
|
+
return m.group(1)
|
|
11106
|
+
return None
|
|
11107
|
+
|
|
11108
|
+
|
|
11109
|
+
def _lc_short(path):
|
|
11110
|
+
"""The ADR dir as the reader would type it: relative to the cwd when that is shorter,
|
|
11111
|
+
absolute otherwise (a different drive on Windows has no relative form)."""
|
|
11112
|
+
if not path:
|
|
11113
|
+
return "docs/adr"
|
|
11114
|
+
try:
|
|
11115
|
+
rel = os.path.relpath(path)
|
|
11116
|
+
except ValueError:
|
|
11117
|
+
return path
|
|
11118
|
+
return rel if len(rel) < len(path) else path
|
|
11119
|
+
|
|
11120
|
+
|
|
11121
|
+
def _lifecycle_report(adr_dir, spec_text):
|
|
11122
|
+
"""Per-component verdicts for every `lifecycle:` entry under `adr_dir`, compared
|
|
11123
|
+
against the go-live declared in `spec_text`. Whole-dimension UNMEASURED -- with the
|
|
11124
|
+
reason spelled out, never silence -- when no ADR carries the block, when the block is
|
|
11125
|
+
unparseable, or when no go-live is declared."""
|
|
11126
|
+
go_live = _lc_go_live(spec_text)
|
|
11127
|
+
comps, unparsed, names = [], [], []
|
|
11128
|
+
if adr_dir and os.path.isdir(adr_dir):
|
|
11129
|
+
names = sorted(f for f in os.listdir(adr_dir) if f.lower().endswith(".md"))
|
|
11130
|
+
for fn in names:
|
|
11131
|
+
try:
|
|
11132
|
+
with open(os.path.join(adr_dir, fn), "r", encoding="utf-8",
|
|
11133
|
+
errors="replace") as fh:
|
|
11134
|
+
lines = [ln.rstrip("\r") for ln in fh.read().split("\n")]
|
|
11135
|
+
except OSError:
|
|
11136
|
+
continue
|
|
11137
|
+
ent = _lc_parse(lines)
|
|
11138
|
+
if ent is None:
|
|
11139
|
+
continue
|
|
11140
|
+
if not ent:
|
|
11141
|
+
unparsed.append(fn)
|
|
11142
|
+
continue
|
|
11143
|
+
for e in ent:
|
|
11144
|
+
comps.append({"adr": fn, "component": e.get("component") or "(unnamed)",
|
|
11145
|
+
"version": e.get("version"), "eol": e.get("eol"),
|
|
11146
|
+
"source": e.get("source"), "checked": e.get("checked")})
|
|
11147
|
+
out = {"status": "UNMEASURED", "reason": None, "go_live": go_live,
|
|
11148
|
+
"adr_dir": adr_dir, "declared": bool(comps or unparsed),
|
|
11149
|
+
"components": comps, "n_components": len(comps), "n_expiring": 0,
|
|
11150
|
+
"unparsed_adrs": unparsed}
|
|
11151
|
+
if not comps:
|
|
11152
|
+
out["reason"] = ("lifecycle: block declared but unparseable in: "
|
|
11153
|
+
+ ", ".join(unparsed) if unparsed else
|
|
11154
|
+
"no ADR under %s carries a lifecycle: block"
|
|
11155
|
+
% _lc_short(adr_dir))
|
|
11156
|
+
return out
|
|
11157
|
+
if not go_live:
|
|
11158
|
+
for c in comps:
|
|
11159
|
+
c["status"], c["detail"] = "unmeasured", "no go-live declared"
|
|
11160
|
+
out["reason"] = ("no go-live declared in the SPEC (frontmatter 'go_live: "
|
|
11161
|
+
"YYYY-MM-DD' or a '**Go-live:** YYYY-MM-DD' line)")
|
|
11162
|
+
return out
|
|
11163
|
+
for c in comps:
|
|
11164
|
+
eol = (c.get("eol") or "").strip()
|
|
11165
|
+
cited = (c.get("source") or "").strip()
|
|
11166
|
+
if not eol or eol.lower() == "unknown" or not _lc_valid_date(eol):
|
|
11167
|
+
c["status"] = "no EOL cited"
|
|
11168
|
+
c["detail"] = ("declared unknown" if eol.lower() == "unknown"
|
|
11169
|
+
else ("not a YYYY-MM-DD date: %s" % eol if eol else "absent"))
|
|
11170
|
+
elif eol < go_live:
|
|
11171
|
+
# the sharper fact wins the label: a date that already expires is reported as
|
|
11172
|
+
# expiring even when uncited, and the missing source stays visible in the record
|
|
11173
|
+
c["status"] = "expires before go-live"
|
|
11174
|
+
c["detail"] = "%s < %s" % (eol, go_live)
|
|
11175
|
+
out["n_expiring"] += 1
|
|
11176
|
+
elif not cited:
|
|
11177
|
+
c["status"] = "no source cited"
|
|
11178
|
+
c["detail"] = "eol %s is asserted, not cited" % eol
|
|
11179
|
+
else:
|
|
11180
|
+
c["status"] = "ok"
|
|
11181
|
+
c["detail"] = "eol %s >= go-live %s" % (eol, go_live)
|
|
11182
|
+
out["status"] = "measured"
|
|
11183
|
+
return out
|
|
11184
|
+
|
|
11185
|
+
|
|
11186
|
+
def _lifecycle_summary(lc):
|
|
11187
|
+
"""The one advisory line, shared by spec-check and readiness so the two surfaces
|
|
11188
|
+
cannot drift apart."""
|
|
11189
|
+
if lc["status"] != "measured":
|
|
11190
|
+
return "lifecycle: UNMEASURED - %s" % lc["reason"]
|
|
11191
|
+
return ("lifecycle: %d component(s), %d expire before go-live %s (advisory)"
|
|
11192
|
+
% (lc["n_components"], lc["n_expiring"], lc["go_live"]))
|
|
11193
|
+
|
|
11194
|
+
|
|
11195
|
+
def _lifecycle_root(*paths):
|
|
11196
|
+
"""The project root a lifecycle read hangs off: the directory of the first path
|
|
11197
|
+
given, else the cwd."""
|
|
11198
|
+
for p in paths:
|
|
11199
|
+
if p:
|
|
11200
|
+
return os.path.dirname(os.path.abspath(p)) or os.getcwd()
|
|
11201
|
+
return os.getcwd()
|
|
11202
|
+
|
|
11203
|
+
|
|
11204
|
+
def _lifecycle_for(root, adr_dir=None, spec_text=None, fallback=True):
|
|
11205
|
+
"""Read the dimension for a project root: `docs/adr` unless overridden, and the
|
|
11206
|
+
go-live from the text already in hand. `fallback` reads `<root>/SPEC.md` when no
|
|
11207
|
+
text was supplied -- it is OFF when the caller named the spec files itself, so an
|
|
11208
|
+
explicit `--spec` is never silently topped up from a file nobody asked for."""
|
|
11209
|
+
if fallback and not _lc_go_live(spec_text):
|
|
11210
|
+
sp = os.path.join(root, "SPEC.md")
|
|
11211
|
+
if os.path.isfile(sp):
|
|
11212
|
+
try:
|
|
11213
|
+
with open(sp, "r", encoding="utf-8", errors="replace") as fh:
|
|
11214
|
+
spec_text = (spec_text or "") + "\n" + fh.read()
|
|
11215
|
+
except OSError:
|
|
11216
|
+
pass
|
|
11217
|
+
return _lifecycle_report(adr_dir or os.path.join(root, "docs", "adr"), spec_text)
|
|
11218
|
+
|
|
11219
|
+
|
|
10989
11220
|
def _spec_check_text(text):
|
|
10990
11221
|
lines = text.split("\n")
|
|
10991
11222
|
n = len(lines)
|
|
@@ -11280,6 +11511,11 @@ def cmd_spec_check(args):
|
|
|
11280
11511
|
m = (_spec_check_text(text) if text.strip()
|
|
11281
11512
|
else {"blockers": [], "untestable": [], "stack_hits": [],
|
|
11282
11513
|
"non_ears": 0, "n_criteria": 0})
|
|
11514
|
+
# lifecycle (ADR-040): read-only and advisory -- it never touches `fail` below.
|
|
11515
|
+
lc = _lifecycle_for(_lifecycle_root(args.spec[0] if args.spec else None,
|
|
11516
|
+
args.acceptance),
|
|
11517
|
+
getattr(args, "adr_dir", None), text,
|
|
11518
|
+
fallback=not args.spec)
|
|
11283
11519
|
structural = len(m["blockers"]) + len(acc_block) # estructura = FACT -> bloquea
|
|
11284
11520
|
soft_find = len(m["untestable"]) + len(m["stack_hits"]) + len(acc_adv)
|
|
11285
11521
|
fail = structural > 0 or (args.strict and soft_find > 0)
|
|
@@ -11288,7 +11524,7 @@ def cmd_spec_check(args):
|
|
|
11288
11524
|
if args.json:
|
|
11289
11525
|
print(json.dumps({"verdict": verdict, "advisory": structural == 0,
|
|
11290
11526
|
"acceptance_blockers": acc_block,
|
|
11291
|
-
"acceptance_advisory": acc_adv, **m},
|
|
11527
|
+
"acceptance_advisory": acc_adv, "lifecycle": lc, **m},
|
|
11292
11528
|
indent=2, ensure_ascii=False))
|
|
11293
11529
|
sys.exit(1 if fail else 0)
|
|
11294
11530
|
|
|
@@ -11309,6 +11545,11 @@ def cmd_spec_check(args):
|
|
|
11309
11545
|
print(f" ~ stack named in criteria ({len(m['stack_hits'])}): {', '.join(terms[:6])} — the 'how' belongs in the ADR, not the SPEC")
|
|
11310
11546
|
if m["non_ears"] and m["n_criteria"]:
|
|
11311
11547
|
print(f" · advisory: {m['non_ears']}/{m['n_criteria']} criteria do not follow the EARS pattern (When/If/While … shall)")
|
|
11548
|
+
print(" · " + _lifecycle_summary(lc))
|
|
11549
|
+
for c in [x for x in lc["components"] if x.get("status") not in ("ok", None)][:5]:
|
|
11550
|
+
print(" %s %s %s - %s (%s)"
|
|
11551
|
+
% ("!" if c["status"] == "expires before go-live" else "~",
|
|
11552
|
+
c["component"], c.get("version") or "?", c["status"], c["detail"]))
|
|
11312
11553
|
print(" i consistency: INFERENTIAL (an uncorrelated checker), not this lint · "
|
|
11313
11554
|
"structure = FACT (blocks) · prose = advisory (--strict to gate)")
|
|
11314
11555
|
if verdict == "OK":
|
|
@@ -12529,6 +12770,9 @@ def build_parser():
|
|
|
12529
12770
|
help="validate ACCEPTANCE traceability (AC-n IDs): missing "
|
|
12530
12771
|
"file / zero criteria / zero traceable / duplicate "
|
|
12531
12772
|
"IDs block as structural FACTS")
|
|
12773
|
+
psc.add_argument("--adr-dir", default=None,
|
|
12774
|
+
help="ADRs whose lifecycle: blocks are read (ADR-040); default "
|
|
12775
|
+
"docs/adr beside the SPEC")
|
|
12532
12776
|
psc.add_argument("--strict", action="store_true",
|
|
12533
12777
|
help="also fail (exit 1) on soft findings: untestable criteria / stack named")
|
|
12534
12778
|
psc.add_argument("--json", action="store_true")
|
|
@@ -137,25 +137,49 @@ answer.
|
|
|
137
137
|
I deduce these core entities: … do they work for you, or is one missing?")
|
|
138
138
|
3. **Operation / API surface.** Propose the endpoints/operations and their
|
|
139
139
|
contracts (idempotency, status codes).
|
|
140
|
-
4. **
|
|
140
|
+
4. **Stack and lifecycle (MANDATORY — before any stack/architecture decision is
|
|
141
|
+
recorded).** The stack is not a given ("we use X"): it is a decision with an EXPIRY
|
|
142
|
+
DATE. A major version is a family; support is granted to a MINOR line, for a window,
|
|
143
|
+
by an upstream nobody in the room controls. Ask these one at a time, each with your
|
|
144
|
+
recommended answer, and **FETCH the dates from the official source AS YOU ASK — never
|
|
145
|
+
answer from memory**; record the URL and the day you checked:
|
|
146
|
+
(a) the EXACT version of every runtime/framework/store (JDK, web framework, ORM, DB,
|
|
147
|
+
Node, bundler, broker, cache) and its OSS/LTS end-of-support date, cited;
|
|
148
|
+
(b) support window vs the declared go-live AND the expected operating life — does the
|
|
149
|
+
line stay patched for the whole operation? If not, move it up BEFORE building: a major
|
|
150
|
+
upgrade days before the milestone is the most expensive one there is;
|
|
151
|
+
(c) major dependencies and the upgrade policy — who approves one, and when it is
|
|
152
|
+
scheduled (aligned with the dev-loop's "zero new dependencies without approval");
|
|
153
|
+
(d) development and observability tools the operator wants from day one (consoles, APM,
|
|
154
|
+
admin UIs) — they CONSTRAIN versions, so ask early or they force the upgrade;
|
|
155
|
+
(e) compatibility with reused legacy modules — the minimum version they support.
|
|
156
|
+
The answers distil into the stack ADR with the dates INSIDE it, as the machine-readable
|
|
157
|
+
`lifecycle:` frontmatter block (`component` / `version` / `eol` / `source` / `checked`,
|
|
158
|
+
ISO dates; `eol: unknown` is allowed and reads as a NAMED absence, never a pass) — see
|
|
159
|
+
`templates/docs/adr/ADR-stack-template.md`. The SPEC declares the milestone it is
|
|
160
|
+
compared against: frontmatter `go_live: YYYY-MM-DD` or a `**Go-live:** YYYY-MM-DD` line.
|
|
161
|
+
`qa_ledger.py spec-check` then reports, per component, `ok` / `expires before go-live` /
|
|
162
|
+
`no EOL cited` / `no source cited` — ADVISORY, it never gates. It can see that a date
|
|
163
|
+
was CITED; it cannot see whether the citation is TRUE. That part is yours.
|
|
164
|
+
5. **Big decisions (→ ADR).** Propose 2–3 architecture options with trade-offs and
|
|
141
165
|
a recommended default: persistence, protocol, idempotency, sync/async, multi-tenancy.
|
|
142
|
-
|
|
166
|
+
6. **Behavior and dirty cases.** Happy path, then failures, retries, partial states,
|
|
143
167
|
concurrency, what must NOT happen.
|
|
144
|
-
|
|
168
|
+
7. **Inviolable constraints (→ `CONSTITUTION.md`).** Domain + security + operation
|
|
145
169
|
rules that can't be broken (money to the cent, no numbering gaps, never cross
|
|
146
170
|
environments, secrets never logged, auth). Write/extend `CONSTITUTION.md` with these —
|
|
147
171
|
one invariant per line, with a CWE reference where it maps. They feed the severity gate
|
|
148
172
|
downstream, and a breach is a BLOCKER, never a trade-off.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
8. **Out of scope.** Explicit boundaries with forward references.
|
|
174
|
+
9. **Acceptance / Definition of Done.** Concrete, checkable criteria + success metrics.
|
|
175
|
+
10. **Quality bar (→ config, kit 1.17.0).** "What level of quality is ENOUGH here, and which
|
|
152
176
|
dimensions are negotiable (coverage, perf, security)?" Propose thresholds fit for
|
|
153
177
|
the risk profile (a payments core is not an internal dashboard). What the human
|
|
154
178
|
declares goes into `uscha.config.json` (`defaults.coverage_threshold`,
|
|
155
179
|
`defaults.readiness_caps`, `defaults.simplicity`) — a declared threshold reads as
|
|
156
180
|
**requerimiento (config)** in the engine's output; an undeclared one stays a kit
|
|
157
181
|
default (opinion) and is labeled as such. Declaring is committing the config.
|
|
158
|
-
|
|
182
|
+
11. **Residual risks and dependencies.** What's uncertain, what must exist first.
|
|
159
183
|
For each HIGH-uncertainty risk, ask (kit 1.19.0, Tip 21 'Prototype to Learn'):
|
|
160
184
|
"Does it warrant a time-boxed spike before freezing the SPEC?" A spike runs on a
|
|
161
185
|
`spike/*` branch and its ONLY legitimate output is an **ADR with lessons**
|
|
@@ -98,6 +98,17 @@ enforcing the record is the engine's job. It is never resolved by "working aroun
|
|
|
98
98
|
- [ ] `golden-diff` clean (byte for byte) = hard closing condition of the touched module
|
|
99
99
|
- [ ] Corpus documented; a module with insufficient corpus = PARTIAL, never COVERS
|
|
100
100
|
|
|
101
|
+
## Stack lifecycle — the stack expires (non-negotiable)
|
|
102
|
+
|
|
103
|
+
> A major version is a family; support is granted to a MINOR line, for a window, by an upstream
|
|
104
|
+
> nobody here controls. A stack ADR that fixes a version without its end-of-support date has not
|
|
105
|
+
> decided anything — it has deferred the decision to the week before go-live (ADR-040).
|
|
106
|
+
> Measured — ADVISORY, never a gate — by `qa_ledger.py spec-check`'s lifecycle dimension.
|
|
107
|
+
|
|
108
|
+
- [ ] No stack ADR without a cited end-of-support date for every component it fixes <!-- ADR-040 -->
|
|
109
|
+
- [ ] The date comes from the OFFICIAL source, fetched when asked, with the URL and the day checked recorded — never from memory
|
|
110
|
+
- [ ] No component whose support ends before the declared `go_live` enters the build; the upgrade happens BEFORE, not days after
|
|
111
|
+
|
|
101
112
|
## Anti-ceremony — Lean over the method itself (meta-invariant)
|
|
102
113
|
|
|
103
114
|
> The risk is not a bad gate: it is the **sum** of good gates turning `/uscha-devloop` into an
|
|
@@ -139,6 +150,12 @@ enforcing the record is the engine's job. It is never resolved by "working aroun
|
|
|
139
150
|
- The **Golden (INV-GOLDEN-01)** invariant is measured by `qa_ledger.py golden-diff`: any `.received`
|
|
140
151
|
that does not match its `.approved` (or is unapproved) = **DIVERGE**, cutting the chain before judgment-day.
|
|
141
152
|
The agent does not touch `.approved` (ideally a `PreToolUse` hook makes it impossible).
|
|
153
|
+
- The **Stack lifecycle** invariant is measured by `qa_ledger.py spec-check`: each `lifecycle:`
|
|
154
|
+
entry in `docs/adr/*.md` is compared against the SPEC's `go_live` and reads `ok` / `expires
|
|
155
|
+
before go-live` / `no EOL cited` / `no source cited`, with the whole dimension UNMEASURED (and
|
|
156
|
+
the reason named) when nothing declares it. **Advisory** — it never gates and never caps
|
|
157
|
+
readiness: the engine can see that a date was CITED, never that the citation is true. Verifying
|
|
158
|
+
the source is the human's job; the record makes the omission visible.
|
|
142
159
|
- The **Anti-ceremony** meta-invariant is not measured by any subcommand: it is the admission filter
|
|
143
160
|
for new gates (does it autorun? does it stay quiet except when it matters? does it collapse into `readiness`? does a
|
|
144
161
|
trivial change skip it?). Its only mechanized leg today is the single verdict of `readiness` (kit
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The machine-readable half of a stack ADR (ADR-040). One entry per component this ADR
|
|
3
|
+
# FIXES: runtime, web framework, ORM, database, Node, bundler, message broker, cache.
|
|
4
|
+
#
|
|
5
|
+
# component the name as your team says it
|
|
6
|
+
# version the EXACT version or minor line you are fixing -- not the major family
|
|
7
|
+
# eol end of OSS/LTS support, YYYY-MM-DD. `unknown` is allowed and reads as a
|
|
8
|
+
# NAMED absence ("no EOL cited"), never as a pass
|
|
9
|
+
# source the OFFICIAL page the date came from. Fetch it while you ask; never
|
|
10
|
+
# answer from memory
|
|
11
|
+
# checked the day you actually looked, YYYY-MM-DD
|
|
12
|
+
#
|
|
13
|
+
# `qa_ledger.py spec-check` reads this block and compares each `eol` against the
|
|
14
|
+
# `go_live` declared in the SPEC. It is ADVISORY: it never gates and never caps
|
|
15
|
+
# readiness. It measures that a date and a source were CITED -- it cannot verify that
|
|
16
|
+
# the source tells the truth. That check is the human's.
|
|
17
|
+
lifecycle:
|
|
18
|
+
- component: <runtime, e.g. the language runtime>
|
|
19
|
+
version: "<exact version or minor line>"
|
|
20
|
+
eol: YYYY-MM-DD
|
|
21
|
+
source: https://<official support page>
|
|
22
|
+
checked: YYYY-MM-DD
|
|
23
|
+
- component: <web framework>
|
|
24
|
+
version: "<exact version or minor line>"
|
|
25
|
+
eol: YYYY-MM-DD
|
|
26
|
+
source: https://<official support page>
|
|
27
|
+
checked: YYYY-MM-DD
|
|
28
|
+
- component: <database / store>
|
|
29
|
+
version: "<exact version or minor line>"
|
|
30
|
+
eol: unknown
|
|
31
|
+
source: https://<official support page>
|
|
32
|
+
checked: YYYY-MM-DD
|
|
33
|
+
---
|
|
34
|
+
# ADR-NNN: <the stack this project fixes, and until when it is supported>
|
|
35
|
+
|
|
36
|
+
## Status: Accepted
|
|
37
|
+
|
|
38
|
+
> The SPEC must declare the milestone this block is compared against, as frontmatter
|
|
39
|
+
> `go_live: YYYY-MM-DD` or a `**Go-live:** YYYY-MM-DD` line. Without it the whole
|
|
40
|
+
> dimension reads UNMEASURED, with the reason named.
|
|
41
|
+
|
|
42
|
+
## Context
|
|
43
|
+
<Why this stack, and what already constrains it: reused legacy modules and the minimum
|
|
44
|
+
versions they support, the operator's development/observability tooling (consoles, APM,
|
|
45
|
+
admin UIs) — those constrain versions, so they are asked BEFORE the stack is fixed, not
|
|
46
|
+
after — and the expected operating life of the system beyond go-live.>
|
|
47
|
+
|
|
48
|
+
## Alternatives
|
|
49
|
+
- A) <option> — support window <until when>, cost <…>
|
|
50
|
+
- B) <option> — support window <until when>, cost <…>
|
|
51
|
+
|
|
52
|
+
## Decision
|
|
53
|
+
- <The exact versions fixed, one line each, with the date they stop being supported.>
|
|
54
|
+
- **Upgrade policy**: <who approves a major upgrade, and when it is scheduled>. A new
|
|
55
|
+
dependency is never added without explicit approval (the dev-loop's "zero new
|
|
56
|
+
dependencies without approval" rule).
|
|
57
|
+
|
|
58
|
+
## Reasons
|
|
59
|
+
- <why this line, and why its support window covers the operation, not just the launch>
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
+ <the good>
|
|
63
|
+
- <the cost: the upgrade already on the calendar, and who owns it>
|
|
64
|
+
|
|
65
|
+
## Implementation Plan
|
|
66
|
+
- Affected paths: <build files, lockfiles, CI images, base images>
|
|
67
|
+
- Tests: <the suite that proves the fixed versions actually run>
|
|
68
|
+
|
|
69
|
+
## Verification
|
|
70
|
+
- [ ] Every component this ADR fixes carries a `version`, an `eol` and the `source` it
|
|
71
|
+
was read from, checked on a named day.
|
|
72
|
+
- [ ] No component's `eol` falls before the SPEC's `go_live`; where one does, the upgrade
|
|
73
|
+
is scheduled BEFORE the build, not after.
|
|
@@ -8,6 +8,11 @@ Format: Status (proposed/accepted/experiment/deprecated/superseded) · Context
|
|
|
8
8
|
Decision · Consequences · Implementation Plan (affected paths, patterns, tests) ·
|
|
9
9
|
Verification (checkboxes).
|
|
10
10
|
|
|
11
|
+
The ADR that fixes the stack has a template of its own, `ADR-stack-template.md`: it carries a
|
|
12
|
+
machine-readable `lifecycle:` frontmatter block (component / version / eol / source / checked)
|
|
13
|
+
that `spec-check` compares against the SPEC's declared `go_live` (ADR-040). Advisory: it reports,
|
|
14
|
+
it never gates.
|
|
15
|
+
|
|
11
16
|
`Status: Experiment` is for a bounded, reversible hypothesis that needs real feedback.
|
|
12
17
|
It must include: Hypothesis, Feedback Signal, Review By or Review Trigger, Promote
|
|
13
18
|
Criteria, and Rollback / Supersede Criteria. Missing/expired metadata is advisory in
|