@codyswann/lisa 4.33.11 → 4.33.13
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/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +53 -4
- package/dist/configs/vitest/scratch-setup.d.ts +34 -0
- package/dist/configs/vitest/scratch-setup.d.ts.map +1 -1
- package/dist/configs/vitest/scratch-setup.js +89 -10
- package/dist/configs/vitest/scratch-setup.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +24 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +53 -4
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +53 -4
- package/plugins/lisa/rules/eager/blocker-containment.md +9 -0
- package/plugins/lisa/rules/eager/deployed-state-readback.md +47 -0
- package/plugins/lisa/rules/reference/blocker-containment.md +13 -0
- package/plugins/lisa/rules/reference/deployed-state-readback.md +90 -0
- package/plugins/lisa/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/lisa/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +53 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-agy/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-agy/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-agy/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +53 -4
- package/plugins/lisa-copilot/rules/eager/blocker-containment.md +9 -0
- package/plugins/lisa-copilot/rules/eager/deployed-state-readback.md +47 -0
- package/plugins/lisa-copilot/rules/reference/blocker-containment.md +13 -0
- package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +90 -0
- package/plugins/lisa-copilot/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-copilot/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-copilot/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-copilot/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +53 -4
- package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +13 -0
- package/plugins/lisa-cursor/rules/blocker-containment.mdc +9 -0
- package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +95 -0
- package/plugins/lisa-cursor/rules/deployed-state-readback.mdc +52 -0
- package/plugins/lisa-cursor/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-cursor/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-cursor/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-cursor/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +53 -4
- package/plugins/src/base/rules/eager/blocker-containment.md +9 -0
- package/plugins/src/base/rules/eager/deployed-state-readback.md +47 -0
- package/plugins/src/base/rules/reference/blocker-containment.md +13 -0
- package/plugins/src/base/rules/reference/deployed-state-readback.md +90 -0
- package/plugins/src/base/skills/lisa-confluence-to-tracker/SKILL.md +8 -1
- package/plugins/src/base/skills/lisa-github-to-tracker/SKILL.md +8 -1
- package/plugins/src/base/skills/lisa-linear-to-tracker/SKILL.md +8 -1
- package/plugins/src/base/skills/lisa-notion-to-tracker/SKILL.md +8 -1
- package/typescript/copy-contents/.husky/pre-commit +10 -6
- package/typescript/copy-contents/.husky/pre-push +46 -16
|
@@ -1019,14 +1019,40 @@ readonly GIT_CMD='(^|[^[:alnum:]_-])git[[:space:]]+'"$GIT_GLOBAL_OPTS"
|
|
|
1019
1019
|
# match. The preceding-char class still excludes `.`, `/`, and `-` to keep
|
|
1020
1020
|
# `--rm`-style flags and `foo.rm` names out.
|
|
1021
1021
|
readonly RM_CMD='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?rm'
|
|
1022
|
+
# Where a recursive-forced flag cluster is allowed to END.
|
|
1023
|
+
#
|
|
1024
|
+
# It used to be `([[:space:]]|$)` — whitespace or end of segment — and a closing
|
|
1025
|
+
# parenthesis is neither, so a SUBSHELL-wrapped pipeline fell out of every guard
|
|
1026
|
+
# built on these two patterns while its brace-wrapped twin was caught: `{ find
|
|
1027
|
+
# .git -type d | parallel rm -rf; }` matched because the `;` supplied the
|
|
1028
|
+
# boundary, and `(find .git -type d | parallel rm -rf)` did not (issue #3494).
|
|
1029
|
+
#
|
|
1030
|
+
# So the two STATEMENT-GROUPING terminators join whitespace here, and nothing
|
|
1031
|
+
# else. The general spelling — "any character that cannot be part of a flag",
|
|
1032
|
+
# `([^[:alnum:]-]|$)` — is the more principled statement of the intent, and is
|
|
1033
|
+
# deliberately NOT used: these patterns feed the rm TARGET WALK, the guard with
|
|
1034
|
+
# this file's longest false-positive history, and it would newly match ordinary
|
|
1035
|
+
# prose punctuation (`echo "rm -rf, then …"` on the comma). Paren and semicolon
|
|
1036
|
+
# buy the brace/paren parity the defect is about and buy nothing else.
|
|
1037
|
+
#
|
|
1038
|
+
# The cluster must still END at the boundary, so `-rfoo` is not read as `-rf`.
|
|
1039
|
+
readonly RM_RF_END='([[:space:];)]|$)'
|
|
1022
1040
|
# rm invoked with BOTH a recursive and a force flag — clustered (-rf/-fr, any
|
|
1023
1041
|
# extra letters) or split, in either order. The split gate pairs ANY recursive
|
|
1024
1042
|
# form (short cluster containing r, or --recursive) with ANY force form (short
|
|
1025
1043
|
# cluster containing f, or --force), so mixed spellings like `rm -r --force /`
|
|
1026
1044
|
# and `rm --recursive -f /` cannot slip between the short/short and long/long
|
|
1027
1045
|
# alternations (PR #1976 review).
|
|
1028
|
-
|
|
1029
|
-
|
|
1046
|
+
#
|
|
1047
|
+
# The FLAG TAILS are named separately from the `rm`-anchored patterns because
|
|
1048
|
+
# the find/xargs family below needs the identical flag vocabulary against a
|
|
1049
|
+
# different verb anchor. Before #3494 it carried its own short-cluster-only copy
|
|
1050
|
+
# and therefore missed `rm --recursive --force` — a divergence that could only
|
|
1051
|
+
# happen because the spelling set was written twice.
|
|
1052
|
+
readonly RM_RF_TAIL_CLUSTER='([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)'"$RM_RF_END"
|
|
1053
|
+
readonly RM_RF_TAIL_SPLIT='(([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*r[[:alnum:]]*|--recursive)([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*f[[:alnum:]]*|--force)'"$RM_RF_END"'|([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*r[[:alnum:]]*|--recursive)'"$RM_RF_END"')'
|
|
1054
|
+
readonly RM_RF_CLUSTER="$RM_CMD$RM_RF_TAIL_CLUSTER"
|
|
1055
|
+
readonly RM_RF_SPLIT="$RM_CMD$RM_RF_TAIL_SPLIT"
|
|
1030
1056
|
|
|
1031
1057
|
# 1. Recursive forced delete (`rm -rf`) of a filesystem root, home, or top-level
|
|
1032
1058
|
# wildcard. Two gates ANDed: the statement must invoke `rm` with BOTH a
|
|
@@ -1667,8 +1693,31 @@ fi
|
|
|
1667
1693
|
if matches '(^|[^[:alnum:]_./-])find[[:space:]][^;&|]*-exec[[:space:]]+rm[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)'; then
|
|
1668
1694
|
block "find -exec rm -rf performs a recursive forced delete on unreviewed paths"
|
|
1669
1695
|
fi
|
|
1670
|
-
|
|
1671
|
-
|
|
1696
|
+
# A command that reads its operands from STDIN and runs a program on them. The
|
|
1697
|
+
# targets are therefore whatever the upstream stage emitted, which is exactly
|
|
1698
|
+
# what makes a recursive forced delete behind one unauditable.
|
|
1699
|
+
#
|
|
1700
|
+
# Three fail-opens closed here (issue #3494), all of them the same mistake —
|
|
1701
|
+
# this rule was written standalone instead of inheriting vocabulary the file
|
|
1702
|
+
# already had:
|
|
1703
|
+
#
|
|
1704
|
+
# 1. The old prefix was `(^|[^[:alnum:]_./-])xargs`, whose excluded class
|
|
1705
|
+
# contains `/`, so the character before `xargs` in `/usr/bin/xargs` was
|
|
1706
|
+
# itself disqualifying and a PATH-SPELLED invocation never matched.
|
|
1707
|
+
# `gxargs` — the GNU findutils spelling shipped by macOS package managers —
|
|
1708
|
+
# failed the same way on the alphanumeric side. RM_CMD above solved this
|
|
1709
|
+
# exact problem for `rm` by putting the boundary before the WHOLE path and
|
|
1710
|
+
# making the directory part optional; that shape is reused verbatim.
|
|
1711
|
+
# 2. `parallel` was absent. It is a drop-in `xargs` substitute reaching the
|
|
1712
|
+
# same delete on the same unaudited stdin, and guard 14 below already had
|
|
1713
|
+
# to name it in its own evidence — it was missing only here.
|
|
1714
|
+
# 3. The flag set was a short-cluster-only copy, so `rm --recursive --force`
|
|
1715
|
+
# was allowed where `rm -rf` was refused. It now uses the SAME tails as the
|
|
1716
|
+
# rm walk, which is what makes that class of divergence unrepeatable.
|
|
1717
|
+
readonly STDIN_DELETER='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(g?xargs|parallel)[[:space:]]([^;&|]*[[:space:]])?rm'
|
|
1718
|
+
if matches "$STDIN_DELETER$RM_RF_TAIL_CLUSTER" \
|
|
1719
|
+
|| matches "$STDIN_DELETER$RM_RF_TAIL_SPLIT"; then
|
|
1720
|
+
block "xargs/parallel rm -rf performs a recursive forced delete on dynamic stdin input"
|
|
1672
1721
|
fi
|
|
1673
1722
|
|
|
1674
1723
|
# 12. Disk destroyers, always on (exceeds upstream, which only scans these
|
|
@@ -37,6 +37,15 @@ A dependency need not deliver code. A blocker **closed as completed** that posit
|
|
|
37
37
|
|
|
38
38
|
This requires a **positive determination**, never an absence. "I could not find a PR" is fail-closed under the rule above; "the item declares it ships no code" is this carve-out. Collapsing the two turns the carve-out into a universal bypass.
|
|
39
39
|
|
|
40
|
+
## Ancestry answers a development question, not a deployment one
|
|
41
|
+
|
|
42
|
+
This rule requires an ancestry test. `deployed-state-readback` says ancestry is **never** evidence of deployment. Both are correct — they answer different questions:
|
|
43
|
+
|
|
44
|
+
- **Here:** *is the blocker's code on the branch I will build from?* An `is blocked by` link is a **development** dependency, satisfied once the code is on your base branch. Ancestry is the right test, and waiting for production strands dependents.
|
|
45
|
+
- **There:** *is this thing live?* A commit can be an ancestor of the default branch and never have shipped — a discarded pipeline execution leaves it merged and undeployed. Ancestry is provably insufficient.
|
|
46
|
+
|
|
47
|
+
Never use this rule's test to answer a "does it already exist in the environment?" question.
|
|
48
|
+
|
|
40
49
|
## Human override
|
|
41
50
|
|
|
42
51
|
A human statement on the item that the dependency is satisfied clears it, and outranks a failed containment check. Record it as the reason in the run record. This is the escape hatch for cross-repo and untracked dependencies, and it is the only one. An automation may never synthesize one on its own behalf.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Deployed-State Readback (load-bearing)
|
|
2
|
+
|
|
3
|
+
**A build ticket asserts that something is missing from a running system. That assertion must come from reading the running system, never from reading the source.** Source tells you what the code says; only the environment tells you what is deployed. A ticket authored from source alone specifies work against a system it has not observed.
|
|
4
|
+
|
|
5
|
+
**One vendor-neutral contract, cited by** the Phase 2 research step of `lisa-notion-to-tracker`, `lisa-github-to-tracker`, `lisa-linear-to-tracker`, and `lisa-confluence-to-tracker` (the `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never four divergent forks).
|
|
6
|
+
|
|
7
|
+
## The waiver this replaces was scoped to the wrong axis
|
|
8
|
+
|
|
9
|
+
Phase 2 has two halves — codebase research and a live product walkthrough — and the walkthrough was waived for work that is *"purely backend with no user-visible surface."*
|
|
10
|
+
|
|
11
|
+
"Is it user-facing?" is a question about the **observation method**: browser, API client, or resource readback. It was being used to answer a different question — **"should we observe live state at all?"** Backend, infrastructure, CI, and dependency work all have live state. It simply is not visible through a browser. Waiving the browser step correctly, and live grounding along with it, is what let tickets be authored for work that had already shipped.
|
|
12
|
+
|
|
13
|
+
So the skip is **not** "no live check". It is **a different probe**.
|
|
14
|
+
|
|
15
|
+
## Routing
|
|
16
|
+
|
|
17
|
+
| The work | Probe |
|
|
18
|
+
|---|---|
|
|
19
|
+
| User-facing surface | `lisa-product-walkthrough` against the running app |
|
|
20
|
+
| HTTP / API backend | Call the deployed endpoint and read the response |
|
|
21
|
+
| Infrastructure / IaC | **Describe the deployed resource in the account** |
|
|
22
|
+
| CI / workflow config | Read the config the running pipeline actually used |
|
|
23
|
+
| Dependency / package pin | Read the **installed** artifact, and the registry |
|
|
24
|
+
| Screen that does not exist yet | No probe; record why |
|
|
25
|
+
|
|
26
|
+
## What is never accepted as evidence of deployment
|
|
27
|
+
|
|
28
|
+
**Git ancestry, a merge check, and `cdk synth` are not deployment evidence.** A commit can be an ancestor of the default branch and never have shipped: a pipeline running `executionMode: SUPERSEDED` discards queued executions, so an intermediate commit's deploy is silently dropped. `git log`, a merge check, and a synth all report "present" while the environment does not have it.
|
|
29
|
+
|
|
30
|
+
Read the deployed thing back. Do not infer it from anything upstream of the deploy.
|
|
31
|
+
|
|
32
|
+
**This does not conflict with `blocker-containment`, which requires exactly the ancestry test this rule rejects.** The two answer different questions and both are right:
|
|
33
|
+
|
|
34
|
+
- `blocker-containment` asks a **development** question — *is the blocker's code on the branch I will build from?* Ancestry is the correct and sufficient test.
|
|
35
|
+
- This rule asks a **deployment** question — *is this live?* Ancestry is provably insufficient, per the discarded-execution case above.
|
|
36
|
+
|
|
37
|
+
Neither may be used to answer the other's question.
|
|
38
|
+
|
|
39
|
+
## An unanswerable probe is not a "no"
|
|
40
|
+
|
|
41
|
+
If the deployed state cannot be read back — no credentials, no such environment, the resource is not addressable — **record that the check could not be performed**. Absence of evidence is not evidence of absence, and defaulting an unreadable probe to "not deployed" reintroduces the whole defect while appearing to satisfy the rule.
|
|
42
|
+
|
|
43
|
+
## Record the readback on the ticket
|
|
44
|
+
|
|
45
|
+
Write what was probed, how, and what came back. Claim-time triage then inherits evidence instead of re-deriving it from source — which is the same mistake one stage later.
|
|
46
|
+
|
|
47
|
+
Full contract (per-lane probe definitions, the discarded-execution hazard, the doctrine this applies, and worked examples): [reference/deployed-state-readback.md](../reference/deployed-state-readback.md).
|
|
@@ -85,6 +85,19 @@ An explicit human statement on the dependent — in the body or in a comment —
|
|
|
85
85
|
|
|
86
86
|
This is the sanctioned escape hatch for the cases the predicate genuinely cannot reach: a cross-repository blocker (two repositories have no shared ancestry, so containment is undefined), a dependency on work tracked outside any connected system, or a vendor whose PR linkage is unreadable. It is the **only** escape hatch. An automation may never synthesize one on its own behalf.
|
|
87
87
|
|
|
88
|
+
## Ancestry here, but never for a deployment question
|
|
89
|
+
|
|
90
|
+
`deployed-state-readback` states that git ancestry, a merge check and `cdk synth` are never evidence that something is deployed. That is not a contradiction of the predicate above; the two rules answer different questions and each uses the test appropriate to its own.
|
|
91
|
+
|
|
92
|
+
| Rule | Question | Ancestry |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| this rule | **Development** — is the blocker's code on the branch I will build from? | Correct and sufficient |
|
|
95
|
+
| `deployed-state-readback` | **Deployment** — is this live? | Provably insufficient |
|
|
96
|
+
|
|
97
|
+
An `is blocked by` link is satisfied once the code you need is on your base branch; whether it has reached production is irrelevant to whether you can build against it — that is the whole point of the #1112 case this rule preserves. A "this already exists, do not build it" claim is satisfied only by reading the running system back, because a discarded pipeline execution leaves a commit merged and unshipped while `git log` still reports it present.
|
|
98
|
+
|
|
99
|
+
**Neither test may be used to answer the other's question.** A surface that needs both runs both.
|
|
100
|
+
|
|
88
101
|
## What this rule does not do
|
|
89
102
|
|
|
90
103
|
It does not decide **when** a dependency edge should exist, dissolve an edge once satisfied, or govern what a surface does after clearing — re-dispatch, re-validation, and lifecycle transitions remain the consuming skill's business. It governs exactly one question: given an edge, is it satisfied.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Deployed-State Readback
|
|
2
|
+
|
|
3
|
+
A build ticket is a claim about a running system: *this is missing, build it.* The claim is only as good as the observation behind it, and until this rule the observation was optional for everything that did not render in a browser.
|
|
4
|
+
|
|
5
|
+
It is a **single vendor-neutral contract** consumed by the Phase 2 research step of `lisa-notion-to-tracker`, `lisa-github-to-tracker`, `lisa-linear-to-tracker`, and `lisa-confluence-to-tracker`. Each cites this slug rather than restating the routing, exactly as they would cite `leaf-only-lifecycle` or `repo-scope-split`. Phase 2 had already forked into four near-identical copies; this is what stops them drifting further.
|
|
6
|
+
|
|
7
|
+
## The failure this replaces
|
|
8
|
+
|
|
9
|
+
Phase 2 is titled "Codebase + Live Product Research" and has two halves:
|
|
10
|
+
|
|
11
|
+
- **2a — codebase research.** Source only, by construction.
|
|
12
|
+
- **2b — live product walkthrough.** The only live-state input anywhere in the PRD-to-ticket path, gated on *"If the PRD touches existing user-facing surfaces"* and then waived outright: *"Skip 2b only when the work is purely backend with no user-visible surface, or affects a screen that does not yet exist in dev/prod."*
|
|
13
|
+
|
|
14
|
+
So user-facing work got one live check and everything else got none. Backend, infrastructure, CI-config and dependency-pin work reached the build queue with **zero deployed-state evidence**, and nothing downstream closed the gap at authoring time: the `*-validate-issue` skills treat "live" as the live *tracker item*; the write skills' only "already exists" concept is the tracker-to-tracker `duplicates` relation; and `lisa-use-the-product`'s IaC lane defaults to `cdk synth` / `cdk diff` — still derived from source — and Phase 2 never routed non-user-facing work into it anyway.
|
|
15
|
+
|
|
16
|
+
### The waiver was scoped to the wrong axis
|
|
17
|
+
|
|
18
|
+
This is the whole diagnosis, and the fix follows from it.
|
|
19
|
+
|
|
20
|
+
*"Is it user-facing?"* is a question about the **observation method** — do you need a browser, an API client, or a resource description call? It was being used to answer *"should we observe live state at all?"*
|
|
21
|
+
|
|
22
|
+
Those come apart precisely outside the browser case. An infrastructure change has live state; a dependency pin has live state; a CI workflow has live state. None of them are visible through a browser, all of them are readable. The waiver treated "not observable *this way*" as "not observable", and every measured failure sat in that gap: tickets aimed at the wrong repository, tickets specifying the defect rather than the fix, tickets whose target version matched none of the four live version numbers in play, tickets for work that needed verification rather than implementation, and a ticket for behavior that had been in production for a month with a code comment already stating its intent verbatim.
|
|
23
|
+
|
|
24
|
+
This is the same shape as `blocker-containment`'s defect: a proxy that holds in the common case, used as though it held in general.
|
|
25
|
+
|
|
26
|
+
## The doctrine this applies
|
|
27
|
+
|
|
28
|
+
Lisa already says this, three times over, in eager load-bearing rules — none of which the authoring path cited:
|
|
29
|
+
|
|
30
|
+
- **`empirical-inquiry`** — *"find out empirically before you act… Do not reason your way to a confident-sounding answer from documentation, prior assumption, or training knowledge when the real system is right there and a quick probe would tell you the truth."*
|
|
31
|
+
- **`verification`** — *"Never claim success without runtime evidence. 'The code looks correct' is not evidence."*
|
|
32
|
+
- **`stale-state-claims`** — on assertions of state that expire. A PRD's *"X doesn't exist yet"* is exactly such a claim, and that rule names this outcome in advance: work *"re-planned as undone when it already shipped."*
|
|
33
|
+
|
|
34
|
+
This rule is not new doctrine. It is those three, wired into the one path that was authoring claims about running systems without consulting any of them.
|
|
35
|
+
|
|
36
|
+
## Routing — the skip is a different probe, never no probe
|
|
37
|
+
|
|
38
|
+
Phase 2 selects a probe for each thing the candidate tickets would create or change:
|
|
39
|
+
|
|
40
|
+
| The work | Probe | Reads |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| User-facing surface | `lisa-product-walkthrough` against `E2E_BASE_URL` | The running app |
|
|
43
|
+
| HTTP / API backend | Call the deployed endpoint as the test identity | The running service |
|
|
44
|
+
| Infrastructure / IaC | Describe the deployed resource in the account (`aws … describe-*`, provider read API) | The account |
|
|
45
|
+
| CI / workflow config | Read the configuration the most recent pipeline run actually used | The pipeline |
|
|
46
|
+
| Dependency / package pin | Read the **installed** artifact in the deployed tree, plus the registry | The artifact |
|
|
47
|
+
| CLI / library | Invoke the published build's `--version` / read-only command | The published build |
|
|
48
|
+
| Screen or resource that does not exist yet | None — record why, per the unanswerable-probe rule below | — |
|
|
49
|
+
|
|
50
|
+
The probe is chosen by **what kind of thing it is**, never by whether a human could have clicked it.
|
|
51
|
+
|
|
52
|
+
## What is never accepted as evidence of deployment
|
|
53
|
+
|
|
54
|
+
**Git ancestry, a merge check, and `cdk synth` are not deployment evidence.**
|
|
55
|
+
|
|
56
|
+
A commit can be an ancestor of the default branch and never have shipped. A deploy pipeline running `executionMode: SUPERSEDED` discards queued executions, so an intermediate commit's deploy is silently dropped. `git log` reports the commit present, a merge check reports it merged, and a synth reports the resource declared — while the environment does not have it. Every one of those signals is upstream of the deploy, and the deploy is the step that can fail silently.
|
|
57
|
+
|
|
58
|
+
Read the deployed thing back. Do not infer it from anything upstream of the deploy.
|
|
59
|
+
|
|
60
|
+
### This does not conflict with `blocker-containment`
|
|
61
|
+
|
|
62
|
+
`blocker-containment` requires exactly the ancestry test this rule rejects. Both are correct, because they answer different questions:
|
|
63
|
+
|
|
64
|
+
| Rule | Question | Ancestry |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `blocker-containment` | **Development** — is the blocker's code on the branch I will build from? | Correct and sufficient |
|
|
67
|
+
| this rule | **Deployment** — is this live? | Provably insufficient |
|
|
68
|
+
|
|
69
|
+
An `is blocked by` link is satisfied when the code you need is on your base branch; whether it has reached production is irrelevant to whether you can build against it. A "this already exists" claim is satisfied only when the thing is running. **Neither test may be used to answer the other's question**, and a surface that needs both runs both.
|
|
70
|
+
|
|
71
|
+
## An unanswerable probe is not a "no"
|
|
72
|
+
|
|
73
|
+
If the deployed state cannot be read back — no credentials for the account, no such environment provisioned, the resource is not addressable from here — **record that the check could not be performed, and say which probe was attempted.**
|
|
74
|
+
|
|
75
|
+
Defaulting an unreadable probe to "not deployed" reintroduces the entire defect while appearing to satisfy this rule, and it is the single most likely way to implement it wrongly. It is the same error `blocker-containment` guards with its fail-closed table and `lisa-repair-intake` guards with *"'No blocker found' is inconclusive, never clearance."* Absence of evidence is not evidence of absence.
|
|
76
|
+
|
|
77
|
+
A ticket authored on an unanswerable probe is still allowed — some work genuinely cannot be grounded before it starts — but it carries the unanswered probe on its face, so the human at the gate and the agent at claim time both know the claim is ungrounded rather than confirmed.
|
|
78
|
+
|
|
79
|
+
## Record the readback on the ticket
|
|
80
|
+
|
|
81
|
+
Write what was probed, how, and what came back — the command or call, and the result. Two reasons:
|
|
82
|
+
|
|
83
|
+
1. **Claim-time triage inherits evidence instead of re-deriving it.** `lisa-ticket-triage`'s `DUPLICATE_ALREADY_FIXED` verdict already demands empirical evidence; without a recorded readback it must reconstruct one from source, which is the same mistake one stage later.
|
|
84
|
+
2. **An ungrounded ticket becomes visible as ungrounded.** A missing readback section is a signal, where a silently source-derived ticket looked identical to a grounded one.
|
|
85
|
+
|
|
86
|
+
## What this rule does not do
|
|
87
|
+
|
|
88
|
+
It does not decide whether the work is worth doing, size it, or govern anything after authoring. It governs one step: before a build ticket asserts that something is absent from a running system, that absence is observed rather than assumed.
|
|
89
|
+
|
|
90
|
+
It also does not require a probe for work that creates something genuinely new — a resource with no deployed predecessor has nothing to read back, and that is the "does not exist yet" row above, recorded rather than skipped silently.
|
|
@@ -282,7 +282,14 @@ Identical to `lisa-notion-to-tracker` Phase 2. Two complementary inputs ground P
|
|
|
282
282
|
|
|
283
283
|
**2b. Live product walkthrough.** If the PRD touches existing user-facing surfaces, invoke the `lisa-product-walkthrough` skill against `E2E_BASE_URL` using the test user from config.
|
|
284
284
|
|
|
285
|
-
Skip
|
|
285
|
+
Skip the *walkthrough* only when the work has no user-visible surface, or affects a screen that does not yet exist in dev/prod — and when you skip it, run 2c instead. Skipping 2b never means authoring the ticket with no live-state grounding.
|
|
286
|
+
|
|
287
|
+
**2c. Deployed-state readback — required whenever 2b does not apply.** Per the `blocker-containment`-style shared-slug precedent, cite the `deployed-state-readback` rule for the full contract; do not restate its routing table here.
|
|
288
|
+
|
|
289
|
+
2b is waived for work with no user-visible surface. That waiver is about the **observation method**, not about whether to observe: backend, infrastructure, CI-config and dependency work all have live state, it is simply not visible through a browser. So the skip selects a **different probe**, never no probe — describe the deployed resource, call the deployed endpoint, read the configuration the last pipeline run used, read the *installed* artifact. For each thing the candidate tickets would create or change, confirm it is genuinely **absent from the deployed environment** before authoring a ticket for it, and record what was probed and what came back.
|
|
290
|
+
|
|
291
|
+
**Git ancestry, a merge check and `cdk synth` are never deployment evidence** — a discarded pipeline execution leaves a commit merged and unshipped, and all three still report "present". **An unanswerable probe is not a "no"**: when the state cannot be read back, record that the check could not be performed rather than defaulting to absent.
|
|
292
|
+
|
|
286
293
|
|
|
287
294
|
Walkthrough findings are attached to the originating Confluence PRD as a **footer comment** (Confluence has no general "page-level discussion attached to a heading" — footer comments are the page-level equivalent), via `lisa-atlassian-access` `operation: comment-page id: <PAGE-ID> kind: footer body: "..."`. Title the comment "Current product walkthrough — `<route>`". Inherited onto the resulting epic / stories under a `## Current Product` subsection.
|
|
288
295
|
|
|
@@ -269,7 +269,14 @@ Identical to `lisa-notion-to-tracker` Phase 2 / `lisa-confluence-to-tracker` / `
|
|
|
269
269
|
|
|
270
270
|
**2b. Live product walkthrough.** If the PRD touches existing user-facing surfaces, invoke `lisa-product-walkthrough` against `E2E_BASE_URL` using the test user from config.
|
|
271
271
|
|
|
272
|
-
Skip
|
|
272
|
+
Skip the *walkthrough* only when the work has no user-visible surface, or affects a screen that does not yet exist in dev/prod — and when you skip it, run 2c instead. Skipping 2b never means authoring the ticket with no live-state grounding.
|
|
273
|
+
|
|
274
|
+
**2c. Deployed-state readback — required whenever 2b does not apply.** Per the `blocker-containment`-style shared-slug precedent, cite the `deployed-state-readback` rule for the full contract; do not restate its routing table here.
|
|
275
|
+
|
|
276
|
+
2b is waived for work with no user-visible surface. That waiver is about the **observation method**, not about whether to observe: backend, infrastructure, CI-config and dependency work all have live state, it is simply not visible through a browser. So the skip selects a **different probe**, never no probe — describe the deployed resource, call the deployed endpoint, read the configuration the last pipeline run used, read the *installed* artifact. For each thing the candidate tickets would create or change, confirm it is genuinely **absent from the deployed environment** before authoring a ticket for it, and record what was probed and what came back.
|
|
277
|
+
|
|
278
|
+
**Git ancestry, a merge check and `cdk synth` are never deployment evidence** — a discarded pipeline execution leaves a commit merged and unshipped, and all three still report "present". **An unanswerable probe is not a "no"**: when the state cannot be read back, record that the check could not be performed rather than defaulting to absent.
|
|
279
|
+
|
|
273
280
|
|
|
274
281
|
Walkthrough findings are surfaced back to product via the orchestrating intake skill (`lisa-github-prd-intake`), which posts them as a comment on the PRD issue. This skill itself does NOT post to GitHub — it only reads. The walkthrough section is also inherited onto the resulting Epic / Stories under a `## Current Product` subsection.
|
|
275
282
|
|
|
@@ -266,7 +266,14 @@ Identical to `lisa-notion-to-tracker` Phase 2 and `lisa-confluence-to-tracker` P
|
|
|
266
266
|
|
|
267
267
|
**2b. Live product walkthrough.** If the PRD touches existing user-facing surfaces, invoke the `lisa-product-walkthrough` skill against `E2E_BASE_URL` using the test user from config.
|
|
268
268
|
|
|
269
|
-
Skip
|
|
269
|
+
Skip the *walkthrough* only when the work has no user-visible surface, or affects a screen that does not yet exist in dev/prod — and when you skip it, run 2c instead. Skipping 2b never means authoring the ticket with no live-state grounding.
|
|
270
|
+
|
|
271
|
+
**2c. Deployed-state readback — required whenever 2b does not apply.** Per the `blocker-containment`-style shared-slug precedent, cite the `deployed-state-readback` rule for the full contract; do not restate its routing table here.
|
|
272
|
+
|
|
273
|
+
2b is waived for work with no user-visible surface. That waiver is about the **observation method**, not about whether to observe: backend, infrastructure, CI-config and dependency work all have live state, it is simply not visible through a browser. So the skip selects a **different probe**, never no probe — describe the deployed resource, call the deployed endpoint, read the configuration the last pipeline run used, read the *installed* artifact. For each thing the candidate tickets would create or change, confirm it is genuinely **absent from the deployed environment** before authoring a ticket for it, and record what was probed and what came back.
|
|
274
|
+
|
|
275
|
+
**Git ancestry, a merge check and `cdk synth` are never deployment evidence** — a discarded pipeline execution leaves a commit merged and unshipped, and all three still report "present". **An unanswerable probe is not a "no"**: when the state cannot be read back, record that the check could not be performed rather than defaulting to absent.
|
|
276
|
+
|
|
270
277
|
|
|
271
278
|
Walkthrough findings are surfaced back to product via the orchestrating intake skill (`lisa-linear-prd-intake`), which posts them on the project's sentinel feedback issue. This skill itself does NOT post to Linear — it only reads. The walkthrough section is also inherited onto the resulting epic / stories under a `## Current Product` subsection in the JIRA description.
|
|
272
279
|
|
|
@@ -254,7 +254,14 @@ Key things to look for:
|
|
|
254
254
|
|
|
255
255
|
**2b. Live product walkthrough.** If the PRD touches existing user-facing surfaces (modifies a screen, adds something next to existing functionality, fixes current behavior, re-styles an existing flow), invoke the `lisa-product-walkthrough` skill against `E2E_BASE_URL` using the test user from config. This grounds the ticket plan in what's actually shipped — design-vs-current-product divergence, reuse candidates, and behavioral surprises that the PRD didn't anticipate become inputs to ticket creation rather than discoveries during implementation.
|
|
256
256
|
|
|
257
|
-
Skip
|
|
257
|
+
Skip the *walkthrough* only when the work has no user-visible surface, or affects a screen that does not yet exist in dev/prod — and when you skip it, run 2c instead. Skipping 2b never means authoring the ticket with no live-state grounding.
|
|
258
|
+
|
|
259
|
+
**2c. Deployed-state readback — required whenever 2b does not apply.** Per the `blocker-containment`-style shared-slug precedent, cite the `deployed-state-readback` rule for the full contract; do not restate its routing table here.
|
|
260
|
+
|
|
261
|
+
2b is waived for work with no user-visible surface. That waiver is about the **observation method**, not about whether to observe: backend, infrastructure, CI-config and dependency work all have live state, it is simply not visible through a browser. So the skip selects a **different probe**, never no probe — describe the deployed resource, call the deployed endpoint, read the configuration the last pipeline run used, read the *installed* artifact. For each thing the candidate tickets would create or change, confirm it is genuinely **absent from the deployed environment** before authoring a ticket for it, and record what was probed and what came back.
|
|
262
|
+
|
|
263
|
+
**Git ancestry, a merge check and `cdk synth` are never deployment evidence** — a discarded pipeline execution leaves a commit merged and unshipped, and all three still report "present". **An unanswerable probe is not a "no"**: when the state cannot be read back, record that the check could not be performed rather than defaulting to absent.
|
|
264
|
+
|
|
258
265
|
|
|
259
266
|
Walkthrough findings are attached to the originating Notion PRD as a comment ("Current product walkthrough — `<route>`") and inherited onto the resulting epic / stories under a `## Current Product` subsection.
|
|
260
267
|
|
|
@@ -243,20 +243,24 @@ elif command -v gitleaks >/dev/null 2>&1; then
|
|
|
243
243
|
fi
|
|
244
244
|
echo "✅ No secrets detected"
|
|
245
245
|
else
|
|
246
|
+
# Blocks rather than warns. This branch used to print the warning below and
|
|
247
|
+
# continue with exit 0, which made "gitleaks is not installed" and "no
|
|
248
|
+
# secrets were found" the same observable: a green commit. A credential scan
|
|
249
|
+
# that cannot run has not cleared the commit, and the lint-staged preflight
|
|
250
|
+
# further down already states the rule this branch was breaking — a check
|
|
251
|
+
# that cannot RUN is not a skip and not a pass.
|
|
246
252
|
echo ""
|
|
247
|
-
echo "
|
|
253
|
+
echo "❌ Commit blocked: Gitleaks is required and is not installed."
|
|
248
254
|
echo ""
|
|
249
|
-
echo "Gitleaks
|
|
255
|
+
echo "Gitleaks is what stops secrets reaching this repository's history, and"
|
|
256
|
+
echo "without it this hook cannot tell a clean commit from an unscanned one."
|
|
250
257
|
echo ""
|
|
251
258
|
echo "To install Gitleaks:"
|
|
252
259
|
echo " macOS: brew install gitleaks"
|
|
253
260
|
echo " Windows: scoop install gitleaks # or choco install gitleaks"
|
|
254
261
|
echo " Linux: See https://github.com/gitleaks/gitleaks#installing"
|
|
255
262
|
echo ""
|
|
256
|
-
|
|
257
|
-
echo ""
|
|
258
|
-
echo "Continuing without secret scanning..."
|
|
259
|
-
echo ""
|
|
263
|
+
exit 1
|
|
260
264
|
fi
|
|
261
265
|
|
|
262
266
|
# Check if native changes require runtime version bump
|
|
@@ -88,6 +88,44 @@ if ! command -v node >/dev/null 2>&1; then
|
|
|
88
88
|
exit 1
|
|
89
89
|
fi
|
|
90
90
|
|
|
91
|
+
# jq is required too, and it is proved HERE rather than at the first step that
|
|
92
|
+
# needs it.
|
|
93
|
+
#
|
|
94
|
+
# jq decides five things in this hook, not one: the dependency-vulnerability
|
|
95
|
+
# audit below, and — through `jq -e '.scripts[...]'` — whether `lint:slow`,
|
|
96
|
+
# `knip:check`, `knip` and `test:mutation` exist at all. A false result from
|
|
97
|
+
# those four probes is indistinguishable from a project that configures none of
|
|
98
|
+
# them, so a machine without jq printed "Skipping slow lint rules (lint:slow
|
|
99
|
+
# not configured)" about a project that configures it, ran no audit at all, and
|
|
100
|
+
# pushed green.
|
|
101
|
+
#
|
|
102
|
+
# That is the failure this whole subsystem exists to prevent: a control
|
|
103
|
+
# returning success for an input it never examined. Absence of the tool and
|
|
104
|
+
# absence of findings produced the identical observable — a clean push — so
|
|
105
|
+
# nothing downstream could tell an audited push from an unaudited one, and the
|
|
106
|
+
# audit is not decoration: it blocks real pushes on real HIGH advisories.
|
|
107
|
+
#
|
|
108
|
+
# The audit block used to warn and continue here, on the reasoning that jq "may
|
|
109
|
+
# legitimately be absent". That was written when jq gated one optional audit.
|
|
110
|
+
# The honest treatment is the one node gets immediately above, and the one the
|
|
111
|
+
# pre-commit lint-staged preflight already states in as many words: a check
|
|
112
|
+
# that cannot RUN is not a skip and not a pass — it blocks.
|
|
113
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
114
|
+
echo "❌ Push blocked: jq is required and is not installed."
|
|
115
|
+
echo ""
|
|
116
|
+
echo " Without jq this hook cannot run the dependency-vulnerability audit,"
|
|
117
|
+
echo " and cannot read package.json to tell whether lint:slow, knip and"
|
|
118
|
+
echo " test:mutation are configured — so it would skip all four and report"
|
|
119
|
+
echo " a clean push it never earned."
|
|
120
|
+
echo ""
|
|
121
|
+
echo "To install jq:"
|
|
122
|
+
echo " macOS: brew install jq"
|
|
123
|
+
echo " Windows: choco install jq # or scoop install jq"
|
|
124
|
+
echo " Linux: apt-get install jq"
|
|
125
|
+
echo ""
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
91
129
|
# BEGIN: push destination guard
|
|
92
130
|
# ---------------------------------------------------------------------------
|
|
93
131
|
# Refuse a push whose RESOLVED destination is a deploy branch it was not aimed
|
|
@@ -388,20 +426,12 @@ fi
|
|
|
388
426
|
# Run security audit
|
|
389
427
|
echo "🔒 Running security audit..."
|
|
390
428
|
|
|
391
|
-
# jq is required for all audit paths (JSON config reading, npm/yarn filtering)
|
|
429
|
+
# jq is required for all audit paths (JSON config reading, npm/yarn filtering),
|
|
430
|
+
# and is proved present at the top of this hook. There is deliberately no
|
|
431
|
+
# "jq is missing, continue anyway" branch here: that branch made a missing tool
|
|
432
|
+
# and a clean audit print the same thing.
|
|
392
433
|
if lisa_gate_covers dependency-vulnerability; then
|
|
393
434
|
echo "ℹ️ Covered by the dependency-vulnerability gate; the built-in audit stands down."
|
|
394
|
-
elif ! command -v jq >/dev/null 2>&1; then
|
|
395
|
-
echo ""
|
|
396
|
-
echo "⚠️ WARNING: jq is not installed - required for security audit"
|
|
397
|
-
echo ""
|
|
398
|
-
echo "To install jq:"
|
|
399
|
-
echo " macOS: brew install jq"
|
|
400
|
-
echo " Windows: choco install jq # or scoop install jq"
|
|
401
|
-
echo " Linux: apt-get install jq"
|
|
402
|
-
echo ""
|
|
403
|
-
echo "Continuing without security audit..."
|
|
404
|
-
echo ""
|
|
405
435
|
else
|
|
406
436
|
# Load GHSA exclusion IDs from JSON config files (managed + project-local)
|
|
407
437
|
load_audit_exclusions() {
|
|
@@ -687,10 +717,10 @@ fi
|
|
|
687
717
|
# did before — slower, but never with the integration tree silently dropped,
|
|
688
718
|
# which is the failure direction that would actually cost something.
|
|
689
719
|
#
|
|
690
|
-
# Resolved with node rather than jq
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
720
|
+
# Resolved with node rather than jq. This used to be a real distinction — jq
|
|
721
|
+
# was optional and node was not — and it stopped being one when jq became a
|
|
722
|
+
# hard requirement at the top of this hook. Either would answer correctly now;
|
|
723
|
+
# node stays because rewriting a working probe buys nothing.
|
|
694
724
|
# The unit script is selected only when it carries LISA_COVERAGE_SCOPE=unit.
|
|
695
725
|
# That marker is not decoration: the threshold factory reads it and enforces the
|
|
696
726
|
# `unit` block, so a `test:cov:unit` without it is measured against the floor
|