@codyswann/lisa 4.29.5 → 4.29.7
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-gates.mjs +20 -2
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +366 -78
- 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 +4 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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/rules/reference/tracked-work.md +19 -0
- package/plugins/lisa-agy/plugin.json +1 -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/rules/reference/tracked-work.md +19 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +19 -0
- 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/rules/reference/tracked-work.md +19 -0
|
@@ -5196,11 +5196,29 @@ export function resolveMoment({
|
|
|
5196
5196
|
costly: definition?.costly === true,
|
|
5197
5197
|
});
|
|
5198
5198
|
}
|
|
5199
|
-
// Rewriters first, then alphabetical within each
|
|
5200
|
-
//
|
|
5199
|
+
// Rewriters first, then CHEAP before COSTLY, then alphabetical within each
|
|
5200
|
+
// group.
|
|
5201
|
+
//
|
|
5202
|
+
// `mayRewrite` leads because a verdict reached before a formatter runs
|
|
5203
|
+
// describes bytes that never ship.
|
|
5204
|
+
//
|
|
5205
|
+
// Cheap-before-costly is about what a failing run costs to learn. A gate
|
|
5206
|
+
// marked `costly` runs a test suite, a browser, or a load generator — minutes
|
|
5207
|
+
// each — and the ones that are not answer in seconds from files already on
|
|
5208
|
+
// disk. Alphabetical order alone put `traceability` behind every `test-*`
|
|
5209
|
+
// gate purely because `tra` sorts after `tes`, so a push refused for
|
|
5210
|
+
// something knowable from the commit range in milliseconds paid the whole
|
|
5211
|
+
// suite first and was told at the end. Nothing about the alphabet was a
|
|
5212
|
+
// statement about cost; this is.
|
|
5213
|
+
//
|
|
5214
|
+
// Ordering only. Every declared gate still runs, still reports its own
|
|
5215
|
+
// verdict, and `verdictFor` still runs a cheap gate after a failure and
|
|
5216
|
+
// stands only the costly ones down — so a run reports everything it can
|
|
5217
|
+
// afford to learn, sooner.
|
|
5201
5218
|
return resolved.sort(
|
|
5202
5219
|
(left, right) =>
|
|
5203
5220
|
Number(right.mayRewrite) - Number(left.mayRewrite) ||
|
|
5221
|
+
Number(left.costly) - Number(right.costly) ||
|
|
5204
5222
|
left.id.localeCompare(right.id)
|
|
5205
5223
|
);
|
|
5206
5224
|
}
|
|
@@ -100,21 +100,44 @@ class TrackingError extends Error {
|
|
|
100
100
|
/** What `soleWorkItem` is reading when it reads a commit. */
|
|
101
101
|
const COMMIT_SUBJECT = "commit message";
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
// WHICH RULE FIRED. `🔗 Work-Item Traceability` is one check name over several
|
|
104
|
+
// independent requirements, and a refusal that does not say which of them it is
|
|
105
|
+
// costs the reader a diagnosis step every single time: the same red check means
|
|
106
|
+
// "rewrite a commit", "edit the body", "post a backlink", or "the tracker says
|
|
107
|
+
// no", and those have nothing in common but the name. Measured on the defect
|
|
108
|
+
// this naming was added for — four misdiagnoses of one refusal in a single day,
|
|
109
|
+
// each one an edit that could not have cleared it.
|
|
110
|
+
//
|
|
111
|
+
// The numbers are the ones `gateSummary` prints, so a refusal and the checklist
|
|
112
|
+
// underneath it refer to the same thing by the same name.
|
|
113
|
+
/** The trailer on each commit in the range. */
|
|
114
|
+
const GATE_COMMIT = "gate 3 (commit trailer)";
|
|
115
|
+
/** The tracker's own answer about an item the range names. */
|
|
116
|
+
const GATE_LIVENESS = "gate 1 (live tracker item)";
|
|
117
|
+
/** What the pull-request body declares, and whether it matches the commits. */
|
|
118
|
+
const GATE_MAPPING = "gate 4 (pull-request declaration)";
|
|
119
|
+
/** The managed backlink comment on the tracker item. */
|
|
120
|
+
const GATE_BACKLINK = "gate 5 (tracker backlink)";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record which rule a refusal belongs to, without overwriting a narrower tag.
|
|
124
|
+
* @param {Error} error The refusal.
|
|
125
|
+
* @param {string} gate The rule that raised it.
|
|
126
|
+
* @returns {Error} The same error, tagged.
|
|
127
|
+
*/
|
|
128
|
+
function taggedGate(error, gate) {
|
|
129
|
+
if (error instanceof TrackingError && !error.gate) error.gate = gate;
|
|
115
130
|
return error;
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
// A HELPER FOR RANGE-WIDE COMMIT REFUSALS USED TO LIVE HERE, and it is gone
|
|
134
|
+
// because the only refusal that used it is gone. `validateCommits` raised
|
|
135
|
+
// "mixed Work-Item references" about the RANGE rather than about any one
|
|
136
|
+
// message; that rule is now the pull-request mapping check in `reportMapping`,
|
|
137
|
+
// where it is about the body and a body edit clears it. Every remaining
|
|
138
|
+
// commit-side refusal comes from `exactWorkItem`, which tags its own by
|
|
139
|
+
// catching.
|
|
140
|
+
|
|
118
141
|
/**
|
|
119
142
|
* The tracker could not be ASKED — a missing binary, a refused credential, a
|
|
120
143
|
* call that never completed. Distinct from a TrackingError, which carries the
|
|
@@ -893,7 +916,7 @@ function exactWorkItem(message, contract = trackerContract()) {
|
|
|
893
916
|
// than inside it keeps the flag set exactly where a commit rewrite is the
|
|
894
917
|
// answer — never on the body refusal, which a rewrite would not touch.
|
|
895
918
|
if (error instanceof TrackingError) error.commitRewritable = true;
|
|
896
|
-
throw error;
|
|
919
|
+
throw taggedGate(error, GATE_COMMIT);
|
|
897
920
|
}
|
|
898
921
|
}
|
|
899
922
|
|
|
@@ -1958,6 +1981,40 @@ function assertStateMatches(ref, contract) {
|
|
|
1958
1981
|
}
|
|
1959
1982
|
}
|
|
1960
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* The binding check for a range that names SEVERAL work items.
|
|
1986
|
+
*
|
|
1987
|
+
* `assertStateMatches` asks whether the binding IS the range's one item, which
|
|
1988
|
+
* a multi-item range has no answer to — picking any single reference out of the
|
|
1989
|
+
* set and comparing against that is a coin toss dressed as a check, and it
|
|
1990
|
+
* would refuse or accept the same push depending on commit order.
|
|
1991
|
+
*
|
|
1992
|
+
* The question that still has an answer is containment: the worktree is bound
|
|
1993
|
+
* to an item, and that item must be one the range actually carries. A branch
|
|
1994
|
+
* bound to something the range never touches is the same mistake the single-ref
|
|
1995
|
+
* check catches — work pushed from a worktree tracking something else — and it
|
|
1996
|
+
* is still refused here. Fails open on an unbound worktree, exactly as
|
|
1997
|
+
* `assertStateMatches` does.
|
|
1998
|
+
* @param {string[]} refs Canonical references the range names, in order.
|
|
1999
|
+
* @param {object} contract Resolved tracker contract.
|
|
2000
|
+
*/
|
|
2001
|
+
function assertStateAmong(refs, contract) {
|
|
2002
|
+
const state = readState(true);
|
|
2003
|
+
if (!state) return;
|
|
2004
|
+
assertStateBranch(state);
|
|
2005
|
+
if (state.provider !== contract.provider) {
|
|
2006
|
+
throw new TrackingError(
|
|
2007
|
+
`Work-item binding provider ${state.provider} does not match configured tracker ${contract.provider}`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
const bound = canonicalizeRef(state.ref, contract);
|
|
2011
|
+
if (refs.includes(bound)) return;
|
|
2012
|
+
throw new TrackingError(
|
|
2013
|
+
`This worktree is bound to ${state.ref}, which none of this range's ` +
|
|
2014
|
+
`work items (${refs.join(", ")}) name`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1961
2018
|
/**
|
|
1962
2019
|
* The work item a branch name encodes, or undefined when it encodes none.
|
|
1963
2020
|
*
|
|
@@ -2046,8 +2103,8 @@ function assertBranchMatches(ref, contract) {
|
|
|
2046
2103
|
* existed, down to the refusal wording.
|
|
2047
2104
|
*
|
|
2048
2105
|
* Scoped to the commit path deliberately. `validateCommits` (push and
|
|
2049
|
-
* pull-request validation) keeps
|
|
2050
|
-
*
|
|
2106
|
+
* pull-request validation) keeps checking the binding directly — against the
|
|
2107
|
+
* range's one reference, or against the set it names — and in CI the head is
|
|
2051
2108
|
* frequently detached or on a synthetic merge ref, where a branch name is not
|
|
2052
2109
|
* a statement about anything.
|
|
2053
2110
|
* @param {string} ref Canonical reference taken from the trailer.
|
|
@@ -2108,18 +2165,43 @@ function validateCommits(commits, configRef, remote) {
|
|
|
2108
2165
|
relevant += 1;
|
|
2109
2166
|
const ref = exactWorkItem(commitMessage(sha), contract);
|
|
2110
2167
|
refs.add(ref);
|
|
2111
|
-
if (!issues.has(ref))
|
|
2168
|
+
if (!issues.has(ref)) {
|
|
2169
|
+
try {
|
|
2170
|
+
issues.set(ref, validateLive(ref, contract));
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
// The tracker's own "no" — closed, missing, out of scope, not a leaf.
|
|
2173
|
+
// Distinct from a badly written trailer, and a rewrite clears neither.
|
|
2174
|
+
throw taggedGate(error, GATE_LIVENESS);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2112
2177
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2178
|
+
// A range spanning SEVERAL work items is no longer refused here, and the
|
|
2179
|
+
// reason is that the refusal was unsatisfiable rather than strict.
|
|
2180
|
+
//
|
|
2181
|
+
// Every commit above has already proved gate 3 on its own terms: one trailer,
|
|
2182
|
+
// canonical, naming an item the tracker says is live, open, repo-scoped and a
|
|
2183
|
+
// leaf. Nothing about that is weakened by the range naming two of them. What
|
|
2184
|
+
// the old refusal enforced was a MAPPING rule — this pull request is about
|
|
2185
|
+
// exactly one item — and a mapping is a property of the pull request, not of
|
|
2186
|
+
// the commits. It is enforced in `validatePrData`, against the pull-request
|
|
2187
|
+
// BODY, which is the only surface where an author can express the answer and
|
|
2188
|
+
// the only one a reviewer can audit.
|
|
2189
|
+
//
|
|
2190
|
+
// Raising it here made the rule impossible to satisfy for a whole class of
|
|
2191
|
+
// legitimate pushes: an integration branch that gathers several finished
|
|
2192
|
+
// items before one pull request has no edit — to any commit, body, or config
|
|
2193
|
+
// — that makes the range name one item, so the only remedies left were to
|
|
2194
|
+
// abandon the shape or to bypass the gate. Neither is a remedy.
|
|
2195
|
+
const list = [...refs];
|
|
2196
|
+
if (list.length === 1) assertStateMatches(list[0], contract);
|
|
2197
|
+
else if (list.length > 1) assertStateAmong(list, contract);
|
|
2198
|
+
const [ref] = list;
|
|
2119
2199
|
return {
|
|
2120
2200
|
contract,
|
|
2121
2201
|
ref,
|
|
2202
|
+
refs: list,
|
|
2122
2203
|
issue: ref ? issues.get(ref) : undefined,
|
|
2204
|
+
issues,
|
|
2123
2205
|
mergeExempt,
|
|
2124
2206
|
protectedExempt,
|
|
2125
2207
|
releaseExempt,
|
|
@@ -2151,14 +2233,35 @@ function remoteDefaultRef(remote) {
|
|
|
2151
2233
|
return exists.status === 0 ? target : undefined;
|
|
2152
2234
|
}
|
|
2153
2235
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2236
|
+
/**
|
|
2237
|
+
* One pushed ref and the commits it introduces, per line git sent.
|
|
2238
|
+
*
|
|
2239
|
+
* PER REF, not pooled, and that is the correction rather than a refactor.
|
|
2240
|
+
* `git push` may carry several ref updates in one invocation — three rebased
|
|
2241
|
+
* branches pushed together to pay one slow pre-push gate instead of three — and
|
|
2242
|
+
* pooling their ranges into a single commit list made the pushed refs
|
|
2243
|
+
* indistinguishable from one branch that had somehow gathered all of their
|
|
2244
|
+
* work. Three branches, each perfectly traced to one work item, were refused
|
|
2245
|
+
* for "mixed Work-Item references" that no branch contained: an artefact of the
|
|
2246
|
+
* pooling, not a property of anything anyone pushed. Batching was therefore
|
|
2247
|
+
* impossible by construction, and N work items cost N full gate runs.
|
|
2248
|
+
*
|
|
2249
|
+
* Each group is validated on its own below, which is exactly how the same
|
|
2250
|
+
* branches would be validated if pushed one at a time. Nothing is exempted:
|
|
2251
|
+
* the rule is unchanged and now applied to the unit it was always about.
|
|
2252
|
+
* @param {string} input The pre-push stdin stream.
|
|
2253
|
+
* @param {string} remote Remote being pushed to.
|
|
2254
|
+
* @returns {{localRef: string|undefined, commits: string[]}[]} One entry per
|
|
2255
|
+
* ref update that introduces commits.
|
|
2256
|
+
*/
|
|
2257
|
+
function parsePushGroups(input, remote) {
|
|
2258
|
+
const groups = [];
|
|
2156
2259
|
// Commits already reachable from the remote default branch are the base's
|
|
2157
2260
|
// history (a merge-sync brings them along); excluding them keeps validation
|
|
2158
2261
|
// scoped to branch-authored commits (issue #1956).
|
|
2159
2262
|
const defaultRef = remoteDefaultRef(remote);
|
|
2160
2263
|
for (const line of input.trim().split(/\r?\n/).filter(Boolean)) {
|
|
2161
|
-
const [, localOid, , remoteOid] = line.trim().split(/\s+/);
|
|
2264
|
+
const [localRef, localOid, , remoteOid] = line.trim().split(/\s+/);
|
|
2162
2265
|
if (!localOid || ZERO_OID.test(localOid)) continue;
|
|
2163
2266
|
const args =
|
|
2164
2267
|
remoteOid && !ZERO_OID.test(remoteOid)
|
|
@@ -2173,16 +2276,20 @@ function parsePushLines(input, remote) {
|
|
|
2173
2276
|
// The existing-branch lane above must NOT use it (its tracking ref is
|
|
2174
2277
|
// pusher-controlled); it excludes only the remote default branch.
|
|
2175
2278
|
["rev-list", localOid, "--not", `--remotes=${remote}`];
|
|
2176
|
-
|
|
2279
|
+
groups.push({
|
|
2280
|
+
localRef,
|
|
2281
|
+
commits: git(args).split("\n").filter(Boolean),
|
|
2282
|
+
});
|
|
2177
2283
|
}
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2180
|
-
|
|
2284
|
+
if (groups.length === 0 && input.trim() === "") {
|
|
2285
|
+
groups.push({
|
|
2286
|
+
localRef: undefined,
|
|
2287
|
+
commits: git(["rev-list", "HEAD", "--not", `--remotes=${remote}`])
|
|
2181
2288
|
.split("\n")
|
|
2182
|
-
.filter(Boolean)
|
|
2183
|
-
);
|
|
2289
|
+
.filter(Boolean),
|
|
2290
|
+
});
|
|
2184
2291
|
}
|
|
2185
|
-
return
|
|
2292
|
+
return groups;
|
|
2186
2293
|
}
|
|
2187
2294
|
|
|
2188
2295
|
/** The `refs/heads/` prefix, spelled once because three parsers below strip it. */
|
|
@@ -2345,16 +2452,34 @@ function validatePushDestination(args) {
|
|
|
2345
2452
|
* @returns {string} The canonical work-item reference.
|
|
2346
2453
|
*/
|
|
2347
2454
|
/**
|
|
2348
|
-
*
|
|
2455
|
+
* Every work item a PULL-REQUEST BODY names, deduplicated, in order.
|
|
2456
|
+
*
|
|
2457
|
+
* The body is where a pull request DECLARES what it is about, and that is why
|
|
2458
|
+
* this reads a set where the commit side reads exactly one. A commit is one
|
|
2459
|
+
* unit of work and can only honestly name one item; a pull request may
|
|
2460
|
+
* legitimately gather several — an integration branch, a stack of finished
|
|
2461
|
+
* items shipped together — and the body is the surface on which saying so is
|
|
2462
|
+
* explicit, reviewable, and visible in the forge without cloning anything.
|
|
2463
|
+
*
|
|
2464
|
+
* Writing N `Work-Item:` trailers into a body is not something that happens by
|
|
2465
|
+
* accident, which is what makes it a declaration rather than a loophole. What
|
|
2466
|
+
* keeps it honest is that `validatePrData` requires the declared set to equal
|
|
2467
|
+
* the set the commits actually carry — so it can neither omit an item the range
|
|
2468
|
+
* contains nor claim one it does not.
|
|
2349
2469
|
*
|
|
2350
|
-
*
|
|
2351
|
-
*
|
|
2470
|
+
* An EMPTY body is still refused, with the same message as before: a pull
|
|
2471
|
+
* request naming no work item at all declares nothing.
|
|
2352
2472
|
* @param {string} body Pull-request body.
|
|
2353
2473
|
* @param {object} contract Resolved tracker contract.
|
|
2354
|
-
* @returns {string} The canonical work-item
|
|
2474
|
+
* @returns {string[]} The canonical work-item references, in order of appearance.
|
|
2355
2475
|
*/
|
|
2356
|
-
function
|
|
2357
|
-
|
|
2476
|
+
function prWorkItems(body, contract) {
|
|
2477
|
+
const values = workItemLines(body);
|
|
2478
|
+
if (values.length === 0)
|
|
2479
|
+
throw new TrackingError(
|
|
2480
|
+
"No Work-Item trailer anywhere in the pull request body"
|
|
2481
|
+
);
|
|
2482
|
+
return [...new Set(values.map(value => canonicalizeRef(value, contract)))];
|
|
2358
2483
|
}
|
|
2359
2484
|
|
|
2360
2485
|
/**
|
|
@@ -2427,15 +2552,16 @@ function commitOutcome(commits, configRef, remote) {
|
|
|
2427
2552
|
* Run one check, recording its refusal as a finding instead of throwing.
|
|
2428
2553
|
* @param {object[]} findings Accumulator.
|
|
2429
2554
|
* @param {string} scope Whether an edit to this pull request can fix it.
|
|
2555
|
+
* @param {string} gate Which rule this check proves.
|
|
2430
2556
|
* @param {Function} check The check to run.
|
|
2431
2557
|
* @returns {unknown} The check's value, or undefined when it refused.
|
|
2432
2558
|
*/
|
|
2433
|
-
function collect(findings, scope, check) {
|
|
2559
|
+
function collect(findings, scope, gate, check) {
|
|
2434
2560
|
try {
|
|
2435
2561
|
return check();
|
|
2436
2562
|
} catch (error) {
|
|
2437
2563
|
if (!(error instanceof TrackingError)) throw error;
|
|
2438
|
-
findings.push({ message: error.message, scope });
|
|
2564
|
+
findings.push({ gate: error.gate ?? gate, message: error.message, scope });
|
|
2439
2565
|
return undefined;
|
|
2440
2566
|
}
|
|
2441
2567
|
}
|
|
@@ -2520,7 +2646,7 @@ function gateSummary(contract) {
|
|
|
2520
2646
|
` 1. the item carries the ready role "${contract.lifecycle.ready}" — required before the work may be created or claimed`,
|
|
2521
2647
|
` 2. the item carries the claimed role "${contract.lifecycle.claimed}" — set when intake dispatches the work; NOT checked here, and no commit is ever refused for it`,
|
|
2522
2648
|
" 3. every commit message carries ONE matching `Work-Item:` trailer — required by the commit-msg hook, on every single commit; exempt are merges, `chore(release)` commits, and commits already on a `deploy.branches` branch (a back-merge or promote re-asserts nothing)",
|
|
2523
|
-
" 4. the pull-request BODY
|
|
2649
|
+
" 4. the pull-request BODY declares EXACTLY the work items its commits carry — one `Work-Item:` line per item, usually one line; a SEPARATE check from gate 3, run at push once a pull request exists and again at CI time; `Closes owner/repo#N` does NOT satisfy it. A range spanning several items is allowed and must name all of them here; naming one the commits do not carry is refused too",
|
|
2524
2650
|
` ${backlink}`,
|
|
2525
2651
|
].join("\n");
|
|
2526
2652
|
}
|
|
@@ -2550,8 +2676,12 @@ function gateSummary(contract) {
|
|
|
2550
2676
|
* @returns {Error} The refusal, with the checklist appended.
|
|
2551
2677
|
*/
|
|
2552
2678
|
function withGateSummary(error) {
|
|
2679
|
+
// The rule's name goes in FRONT of its own message, not only in the checklist
|
|
2680
|
+
// below it: the checklist says what all five gates are, and the reader still
|
|
2681
|
+
// has to work out which one they are looking at. See `GATE_COMMIT`.
|
|
2682
|
+
const rule = error.gate ? `Work-Item Traceability ${error.gate}: ` : "";
|
|
2553
2683
|
return new TrackingError(
|
|
2554
|
-
`${error.message}\n${gateSummary(trackerContract())}`
|
|
2684
|
+
`${rule}${error.message}\n${gateSummary(trackerContract())}`
|
|
2555
2685
|
);
|
|
2556
2686
|
}
|
|
2557
2687
|
|
|
@@ -2571,12 +2701,87 @@ function requirementReport(findings, contract) {
|
|
|
2571
2701
|
: `${ordered.length} work-item traceability requirements are unmet. Every one of them is listed here, hardest first, so a single pass can clear them:`;
|
|
2572
2702
|
const body = ordered
|
|
2573
2703
|
.map(
|
|
2574
|
-
(finding, index) =>
|
|
2704
|
+
(finding, index) =>
|
|
2705
|
+
`${index + 1}. ${finding.gate ? `${finding.gate} ` : ""}${finding.scope} ${finding.message}`
|
|
2575
2706
|
)
|
|
2576
2707
|
.join("\n\n");
|
|
2577
2708
|
return `${head}\n\n${body}\n${gateSummary(contract)}`;
|
|
2578
2709
|
}
|
|
2579
2710
|
|
|
2711
|
+
/**
|
|
2712
|
+
* What the pull-request body must say when the range spans several items.
|
|
2713
|
+
*
|
|
2714
|
+
* Named in the refusal, because "declare it in the body" is a remedy a reader
|
|
2715
|
+
* has to be able to act on without going looking for the syntax.
|
|
2716
|
+
* @param {string[]} refs The references the body is missing.
|
|
2717
|
+
* @returns {string} Remediation sentence.
|
|
2718
|
+
*/
|
|
2719
|
+
function declarationAdvice(refs) {
|
|
2720
|
+
const lines = refs.map(ref => ` Work-Item: ${ref}`).join("\n");
|
|
2721
|
+
return (
|
|
2722
|
+
`A pull request may gather several work items, but it has to SAY so: add ` +
|
|
2723
|
+
`one \`Work-Item:\` line per item to the pull-request body, so the body ` +
|
|
2724
|
+
`names exactly the set its commits carry. Add:\n\n${lines}\n\n` +
|
|
2725
|
+
`If this range was not meant to span several items, the answer is the ` +
|
|
2726
|
+
`other one: take the foreign commits out of it.`
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/**
|
|
2731
|
+
* The mapping rule — the pull request declares exactly the items it carries.
|
|
2732
|
+
*
|
|
2733
|
+
* This is the rule that used to be enforced on the commit range as "no mixed
|
|
2734
|
+
* references", where it was unsatisfiable for an integration branch. Moving it
|
|
2735
|
+
* here does not retire it; it gives it the one surface on which it has an
|
|
2736
|
+
* answer. Set EQUALITY, in both directions, is what keeps it a check:
|
|
2737
|
+
*
|
|
2738
|
+
* - an item in the commits but not the body is undeclared work, which is the
|
|
2739
|
+
* accidental mix the old rule was aimed at, and it is still refused;
|
|
2740
|
+
* - an item in the body but not the commits is a claim the range does not
|
|
2741
|
+
* support — padding the declaration to make a refusal go away — and it is
|
|
2742
|
+
* refused too, which is what stops "list everything" from being a bypass.
|
|
2743
|
+
*
|
|
2744
|
+
* Silent when either side is empty: those are gate-3 and gate-4 absences, and
|
|
2745
|
+
* they are already reported by their own findings. Saying it twice would make
|
|
2746
|
+
* one missing trailer read as two unmet requirements.
|
|
2747
|
+
* @param {object[]} findings Accumulator.
|
|
2748
|
+
* @param {string[]} commitRefs References the range's commits carry.
|
|
2749
|
+
* @param {string[]} bodyRefs References the pull-request body declares.
|
|
2750
|
+
*/
|
|
2751
|
+
function reportMapping(findings, commitRefs, bodyRefs) {
|
|
2752
|
+
if (commitRefs.length === 0 || bodyRefs.length === 0) return;
|
|
2753
|
+
const undeclared = commitRefs.filter(ref => !bodyRefs.includes(ref));
|
|
2754
|
+
const unsupported = bodyRefs.filter(ref => !commitRefs.includes(ref));
|
|
2755
|
+
// The 1:1 case keeps its own sentence. "Body declares X, commits carry Y" is
|
|
2756
|
+
// one disagreement, and splitting it into a missing item plus a spurious one
|
|
2757
|
+
// describes a single typo as two faults.
|
|
2758
|
+
if (commitRefs.length === 1 && bodyRefs.length === 1 && undeclared.length) {
|
|
2759
|
+
findings.push({
|
|
2760
|
+
gate: GATE_MAPPING,
|
|
2761
|
+
message: `Pull request Work-Item ${bodyRefs[0]} does not match commit Work-Item ${commitRefs[0]}`,
|
|
2762
|
+
scope: IN_THIS_PR,
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (undeclared.length > 0)
|
|
2767
|
+
findings.push({
|
|
2768
|
+
gate: GATE_MAPPING,
|
|
2769
|
+
message:
|
|
2770
|
+
`Pull request body does not declare ${undeclared.join(", ")}, which ` +
|
|
2771
|
+
`this range's commits carry. ${declarationAdvice(commitRefs)}`,
|
|
2772
|
+
scope: IN_THIS_PR,
|
|
2773
|
+
});
|
|
2774
|
+
if (unsupported.length > 0)
|
|
2775
|
+
findings.push({
|
|
2776
|
+
gate: GATE_MAPPING,
|
|
2777
|
+
message:
|
|
2778
|
+
`Pull request body declares ${unsupported.join(", ")}, which no ` +
|
|
2779
|
+
`commit in this range carries. The body must name exactly the items ` +
|
|
2780
|
+
`the commits do — no more, or the declaration stops meaning anything.`,
|
|
2781
|
+
scope: IN_THIS_PR,
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2580
2785
|
/**
|
|
2581
2786
|
* Check every pull-request requirement and report all of the unmet ones.
|
|
2582
2787
|
* @param {{result?: object, error?: Error}} outcome Commit-side outcome.
|
|
@@ -2612,6 +2817,7 @@ function validatePrData(outcome, prUrl, prBody) {
|
|
|
2612
2817
|
// advice is what makes the difference legible at the point of reading.
|
|
2613
2818
|
if (outcome.error)
|
|
2614
2819
|
findings.push({
|
|
2820
|
+
gate: outcome.error.gate ?? GATE_COMMIT,
|
|
2615
2821
|
message: outcome.error.commitRewritable
|
|
2616
2822
|
? `${outcome.error.message}. ${COMMIT_REWRITE_ADVICE}`
|
|
2617
2823
|
: outcome.error.message,
|
|
@@ -2619,35 +2825,42 @@ function validatePrData(outcome, prUrl, prBody) {
|
|
|
2619
2825
|
});
|
|
2620
2826
|
else if (!tracedWhereAuthored && result.relevant === 0)
|
|
2621
2827
|
findings.push({
|
|
2828
|
+
gate: GATE_COMMIT,
|
|
2622
2829
|
message: `Pull request has no non-merge commit linked to a work item. ${COMMIT_REWRITE_ADVICE}`,
|
|
2623
2830
|
scope: IN_THIS_PR,
|
|
2624
2831
|
});
|
|
2625
2832
|
else if (!tracedWhereAuthored && !result.ref)
|
|
2626
2833
|
findings.push({
|
|
2834
|
+
gate: GATE_COMMIT,
|
|
2627
2835
|
message: `Pull request commits are not linked to a work item. ${COMMIT_REWRITE_ADVICE}`,
|
|
2628
2836
|
scope: IN_THIS_PR,
|
|
2629
2837
|
});
|
|
2630
|
-
const
|
|
2631
|
-
const
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
scope: IN_THIS_PR,
|
|
2638
|
-
});
|
|
2639
|
-
const ref = commitRef ?? bodyRef;
|
|
2838
|
+
const commitRefs = result?.refs ?? [];
|
|
2839
|
+
const bodyRefs =
|
|
2840
|
+
collect(findings, IN_THIS_PR, GATE_MAPPING, () =>
|
|
2841
|
+
prWorkItems(prBody, contract)
|
|
2842
|
+
) ?? [];
|
|
2843
|
+
reportMapping(findings, commitRefs, bodyRefs);
|
|
2844
|
+
const refs = commitRefs.length > 0 ? commitRefs : bodyRefs;
|
|
2640
2845
|
// Requirement 4 belongs to `full` alone: it needs tracker WRITE access, and
|
|
2641
2846
|
// a project that keeps no tracker credentials cannot ever satisfy it. The
|
|
2642
2847
|
// reference checks above are what stays, and they still refuse everything
|
|
2643
2848
|
// that is genuinely untraceable.
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2849
|
+
//
|
|
2850
|
+
// EVERY item the range names needs its own backlink, not just the first one.
|
|
2851
|
+
// A pull request that gathers three items and links one of them leaves the
|
|
2852
|
+
// other two with no route back from the tracker, which is the whole property
|
|
2853
|
+
// this gate exists to keep — and a multi-item range that proved less than a
|
|
2854
|
+
// single-item one would make the declaration a way of buying weaker checks.
|
|
2855
|
+
if (contract.verify === "full") {
|
|
2856
|
+
for (const ref of refs) {
|
|
2857
|
+
const before = findings.length;
|
|
2858
|
+
collect(findings, OUTSIDE_THIS_PR, GATE_BACKLINK, () =>
|
|
2859
|
+
assertBacklink(ref, prUrl, contract, result?.issues?.get(ref))
|
|
2860
|
+
);
|
|
2861
|
+
if (findings.length > before)
|
|
2862
|
+
findings[before].message += `. ${backlinkAdvice(ref, prUrl, contract)}`;
|
|
2863
|
+
}
|
|
2651
2864
|
}
|
|
2652
2865
|
if (findings.length > 0)
|
|
2653
2866
|
throw new TrackingError(requirementReport(findings, contract));
|
|
@@ -3494,33 +3707,108 @@ function validateCommit(args) {
|
|
|
3494
3707
|
console.log(`WORK_ITEM_TRACKING_OK ${result.exempt ?? result.ref}`);
|
|
3495
3708
|
}
|
|
3496
3709
|
|
|
3710
|
+
/**
|
|
3711
|
+
* The pushed refs worth validating, with the empty push still one answer.
|
|
3712
|
+
*
|
|
3713
|
+
* A push whose refs introduce no commits is not nothing to report: this command
|
|
3714
|
+
* has always answered `0 commit(s)` there, and the pull-request checks still
|
|
3715
|
+
* run against it. Dropping every empty group without a replacement would turn
|
|
3716
|
+
* that answer into silence.
|
|
3717
|
+
* @param {string} input The pre-push stdin stream.
|
|
3718
|
+
* @param {string} remote Remote being pushed to.
|
|
3719
|
+
* @returns {{localRef: string|undefined, commits: string[]}[]} Groups to check.
|
|
3720
|
+
*/
|
|
3721
|
+
function pushGroups(input, remote) {
|
|
3722
|
+
const parsed = parsePushGroups(input, remote);
|
|
3723
|
+
const carrying = parsed.filter(group => group.commits.length > 0);
|
|
3724
|
+
return carrying.length > 0
|
|
3725
|
+
? carrying
|
|
3726
|
+
: [{ localRef: parsed[0]?.localRef, commits: [] }];
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
/**
|
|
3730
|
+
* Which pushed ref the pull request in hand is actually about.
|
|
3731
|
+
*
|
|
3732
|
+
* `gh pr view` answers for the CURRENT BRANCH, so its body and backlink are
|
|
3733
|
+
* evidence about that branch and nothing else. Checking a second branch's range
|
|
3734
|
+
* against it would report another branch's work item as missing from a body
|
|
3735
|
+
* that was never supposed to name it.
|
|
3736
|
+
* @param {object[]} groups The pushed refs.
|
|
3737
|
+
* @param {object|undefined} pr The pull request, when one exists.
|
|
3738
|
+
* @returns {object|undefined} The group the pull request describes.
|
|
3739
|
+
*/
|
|
3740
|
+
function prTargetGroup(groups, pr) {
|
|
3741
|
+
if (!pr) return undefined;
|
|
3742
|
+
if (groups.length === 1) return groups[0];
|
|
3743
|
+
const branch = activeBranch();
|
|
3744
|
+
return groups.find(
|
|
3745
|
+
group => branch && pushedBranchName(group.localRef ?? "") === branch
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
/**
|
|
3750
|
+
* What a push could not prove here, named rather than left to CI to reveal.
|
|
3751
|
+
*
|
|
3752
|
+
* The multi-item sentence is the one that matters. A range naming several work
|
|
3753
|
+
* items is legitimate, and it is also the shape most likely to be an accident —
|
|
3754
|
+
* so the moment it is observed, the operator is told what CI will require of
|
|
3755
|
+
* it: a body declaring every one of them. Refusing here instead would be a
|
|
3756
|
+
* refusal with no remedy, because the body it asks for belongs to a pull
|
|
3757
|
+
* request that cannot exist until this push lands.
|
|
3758
|
+
* @param {object} result Commit-side result.
|
|
3759
|
+
* @returns {string} The clause after the commit count.
|
|
3760
|
+
*/
|
|
3761
|
+
function pushDeferralNote(result) {
|
|
3762
|
+
const refs = result.refs ?? [];
|
|
3763
|
+
const spanning =
|
|
3764
|
+
refs.length > 1
|
|
3765
|
+
? ` This range names ${refs.length} work items (${refs.join(", ")}): allowed, but the pull-request body must then declare every one of them, and ${GATE_MAPPING} refuses it at CI time if it does not.`
|
|
3766
|
+
: "";
|
|
3767
|
+
return `no pull request exists yet, so gates 4 and 5 could not be checked here — CI will verify both.${spanning}`;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/**
|
|
3771
|
+
* Report one pushed ref's outcome, refusing when it did not prove out.
|
|
3772
|
+
* @param {{result?: object, error?: Error}} outcome Commit-side outcome.
|
|
3773
|
+
* @param {object|undefined} pr The pull request this ref is about, if any.
|
|
3774
|
+
* @param {string} label Prefix naming the ref, empty for a single-ref push.
|
|
3775
|
+
*/
|
|
3776
|
+
function reportPushGroup(outcome, pr, label) {
|
|
3777
|
+
if (pr) {
|
|
3778
|
+
validatePrData(outcome, pr.url, pr.body);
|
|
3779
|
+
console.log(
|
|
3780
|
+
`WORK_ITEM_TRACKING_OK ${label}${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}, ${provedHere(outcome.result.contract)}`
|
|
3781
|
+
);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
// No pull request means gates 4 and 5 cannot be CHECKED here. They are
|
|
3785
|
+
// still perfectly well KNOWN here, and this is the last local moment before
|
|
3786
|
+
// CI — so the checklist goes out either way. Saying nothing is precisely
|
|
3787
|
+
// what made those two gates separate CI-cycle surprises (#2681).
|
|
3788
|
+
if (outcome.error) throw withGateSummary(outcome.error);
|
|
3789
|
+
console.log(
|
|
3790
|
+
`WORK_ITEM_TRACKING_OK ${label}${outcome.result.relevant} commit(s); ${pushDeferralNote(outcome.result)}${gateSummary(outcome.result.contract)}`
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3497
3794
|
function validatePush(args) {
|
|
3498
3795
|
const remote = args[0] || "origin";
|
|
3499
3796
|
const input = readFileSync(0, "utf8");
|
|
3500
3797
|
// The deploy chain is read from the remote default branch, the same ref this
|
|
3501
3798
|
// path already trusts to bound the range. A local working tree could declare
|
|
3502
3799
|
// anything, but so could a `--no-verify`; CI is the enforcing copy.
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
remoteDefaultRef(remote),
|
|
3506
|
-
remote
|
|
3507
|
-
);
|
|
3800
|
+
const configRef = remoteDefaultRef(remote);
|
|
3801
|
+
const groups = pushGroups(input, remote);
|
|
3508
3802
|
const pr = currentPullRequest();
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
`WORK_ITEM_TRACKING_OK ${outcome.result.relevant} commit(s); no pull request exists yet, so gates 4 and 5 could not be checked here — CI will verify both${gateSummary(outcome.result.contract)}`
|
|
3803
|
+
const target = prTargetGroup(groups, pr);
|
|
3804
|
+
for (const group of groups) {
|
|
3805
|
+
const outcome = commitOutcome(group.commits, configRef, remote);
|
|
3806
|
+
reportPushGroup(
|
|
3807
|
+
outcome,
|
|
3808
|
+
group === target ? pr : undefined,
|
|
3809
|
+
groups.length > 1 ? `${group.localRef ?? "(stdin)"}: ` : ""
|
|
3517
3810
|
);
|
|
3518
|
-
return;
|
|
3519
3811
|
}
|
|
3520
|
-
validatePrData(outcome, pr.url, pr.body);
|
|
3521
|
-
console.log(
|
|
3522
|
-
`WORK_ITEM_TRACKING_OK ${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}, ${provedHere(outcome.result.contract)}`
|
|
3523
|
-
);
|
|
3524
3812
|
}
|
|
3525
3813
|
|
|
3526
3814
|
function validatePr(args) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA43BjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA+xBjC,CAAC"}
|