@codyswann/lisa 4.70.1 → 4.70.3

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.
Files changed (72) hide show
  1. package/README.md +18 -0
  2. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +302 -5
  3. package/all/copy-overwrite/scripts/lisa-work-item.mjs +71 -10
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +16 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  8. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.js +7 -4
  10. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  11. package/dist/migrations/index.d.ts +1 -0
  12. package/dist/migrations/index.d.ts.map +1 -1
  13. package/dist/migrations/index.js +3 -0
  14. package/dist/migrations/index.js.map +1 -1
  15. package/dist/migrations/prune-retired-entire-hooks.d.ts +25 -0
  16. package/dist/migrations/prune-retired-entire-hooks.d.ts.map +1 -0
  17. package/dist/migrations/prune-retired-entire-hooks.js +146 -0
  18. package/dist/migrations/prune-retired-entire-hooks.js.map +1 -0
  19. package/package.json +4 -4
  20. package/plugins/lisa/.claude-plugin/plugin.json +1 -64
  21. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/.claude-plugin/plugin.json +0 -63
  71. package/rails/merge/.claude/settings.json +0 -47
  72. package/scripts/install-claude-plugins.sh +1 -1
package/README.md CHANGED
@@ -259,6 +259,24 @@ servers, or configuration. Other harnesses retain their existing delivery
259
259
  behavior. For a new project, run the CLI ephemerally with
260
260
  `bunx @codyswann/lisa setup-project ...`.
261
261
 
262
+ Entire session capture is host-owned opt-in. Lisa's Rails settings and base
263
+ Claude plugin register no Entire commands. Full apply also removes the seven
264
+ exact automatic command strings shipped by older Lisa releases, because an
265
+ array-union merge cannot retire installed entries by itself. Other hooks and
266
+ settings survive, and Lisa does not inspect or delete existing session data.
267
+ This does not change user/global settings or registrations owned by other
268
+ plugins.
269
+
270
+ To opt in, review capture, retention, and access first, then create a host-owned
271
+ wrapper such as `scripts/private-session-hooks.sh`. Register its distinct
272
+ command in Claude's project hook settings, for example
273
+ `bash "$CLAUDE_PROJECT_DIR/scripts/private-session-hooks.sh" session-start`.
274
+ Lisa preserves that wrapper on reapply. Reusing an exact retired Lisa command
275
+ does not establish host ownership and the migration will remove it. Keep the
276
+ Lisa governance hooks enabled; no Entire configuration flag is required or
277
+ interpreted by Lisa. Start a fresh session after updating the installed plugin
278
+ so it loads the current registrations.
279
+
262
280
  Successful starter creation records the repository, tracked branch and copied
263
281
  revision in `starter.templates`. Git clones record their own head before setup
264
282
  replaces the clone history; GitHub template creation checks the copied tree
@@ -222,6 +222,301 @@ export const qualifyScanner = requested => {
222
222
  return binary;
223
223
  };
224
224
 
225
+ const EVIDENCE_BUDGET = 64 * 1024 * 1024;
226
+ const NO_REPLACEMENTS = "--no-replace-objects";
227
+ const TAXONOMY = "Disk/missing/corrupt";
228
+ const NARRATIVE = [
229
+ "AccessDenied boundaries,",
230
+ `${TAXONOMY} falsifications.`,
231
+ ].join(" ");
232
+ const safePath = value =>
233
+ typeof value === "string" &&
234
+ value.length > 0 &&
235
+ !/[\\\u0000-\u001f\u007f]/u.test(value) &&
236
+ !/^(?:\/|[A-Za-z]:)/u.test(value) &&
237
+ value.split("/").every(part => part && part !== "." && part !== "..");
238
+
239
+ /** Read authentic regular Git blobs, never trimmed text or worktree evidence. */
240
+ const readBlob = (file, commit, cwd, budget) => {
241
+ if (!safePath(file)) return null;
242
+ const entries = gitRead(
243
+ ["--literal-pathspecs", "ls-tree", "-z", commit, "--", file],
244
+ cwd
245
+ )
246
+ .split("\0")
247
+ .filter(Boolean);
248
+ if (entries.length !== 1) return null;
249
+ const entry = entries[0].match(
250
+ /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([\s\S]*)$/u
251
+ );
252
+ if (!entry || entry[3] !== file || entry[2].length !== commit.length)
253
+ return null;
254
+ const sizeText = gitRead(["cat-file", "-s", entry[2]], cwd);
255
+ const size = Number(sizeText);
256
+ if (!/^\d+$/u.test(sizeText) || !Number.isSafeInteger(size))
257
+ throw new HistorySecretError(
258
+ "Git blob size is invalid. Repair complete Git objects and retry; safety is unproved."
259
+ );
260
+ if (size > budget.remaining) return null;
261
+ const result = spawnSync(
262
+ "git",
263
+ [NO_REPLACEMENTS, "cat-file", "blob", entry[2]],
264
+ {
265
+ cwd,
266
+ env: gitEnvironment(),
267
+ timeout: 120000,
268
+ maxBuffer: EVIDENCE_BUDGET,
269
+ }
270
+ );
271
+ if (
272
+ result.error ||
273
+ result.signal ||
274
+ result.status !== 0 ||
275
+ result.stdout.length !== size ||
276
+ createHash(commit.length === 40 ? "sha1" : "sha256")
277
+ .update(`blob ${size}\0`)
278
+ .update(result.stdout)
279
+ .digest("hex") !== entry[2]
280
+ )
281
+ throw new HistorySecretError(
282
+ "Git blob bytes cannot be verified. Repair complete Git objects and retry; no raw source is published."
283
+ );
284
+ budget.remaining -= size;
285
+ return result.stdout;
286
+ };
287
+
288
+ /** Native JSON validation plus bounded duplicate-aware lexical role attribution. */
289
+ const evidenceMap = bytes => {
290
+ if (bytes.length > 1024 * 1024) return null;
291
+ const text = bytes.toString("utf8");
292
+ if (!Buffer.from(text).equals(bytes)) return null;
293
+ try {
294
+ const value = JSON.parse(text);
295
+ if (
296
+ !value ||
297
+ typeof value !== "object" ||
298
+ Array.isArray(value) ||
299
+ !value.source_sha256 ||
300
+ typeof value.source_sha256 !== "object" ||
301
+ Array.isArray(value.source_sha256)
302
+ )
303
+ return null;
304
+ const entries = Object.entries(value.source_sha256);
305
+ if (
306
+ !entries.length ||
307
+ entries.length > 256 ||
308
+ entries.some(
309
+ ([file, hash]) =>
310
+ !safePath(file) ||
311
+ typeof hash !== "string" ||
312
+ !/^[a-f0-9]{64}$/u.test(hash)
313
+ )
314
+ )
315
+ return null;
316
+ const tokens = [
317
+ ...text.matchAll(/"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^{}[\],:\s]+/gu),
318
+ ];
319
+ if (tokens.length > 8192) return null;
320
+ let cursor = 0;
321
+ const spans = [];
322
+ const visit = (path, depth) => {
323
+ if (depth > 32) throw new Error("Bounded JSON depth");
324
+ const token = tokens[cursor++];
325
+ if (token[0] === "{") {
326
+ const keys = new Set();
327
+ while (tokens[cursor][0] !== "}") {
328
+ const key = JSON.parse(tokens[cursor++][0]);
329
+ if (keys.has(key)) throw new Error("Duplicate decoded JSON key");
330
+ keys.add(key);
331
+ cursor++; // Native JSON validation already proved the colon.
332
+ visit([...path, key], depth + 1);
333
+ if (tokens[cursor][0] === ",") cursor++;
334
+ }
335
+ cursor++;
336
+ } else if (token[0] === "[") {
337
+ let index = 0;
338
+ while (tokens[cursor][0] !== "]") {
339
+ visit([...path, index++], depth + 1);
340
+ if (tokens[cursor][0] === ",") cursor++;
341
+ }
342
+ cursor++;
343
+ } else if (
344
+ path.length === 2 &&
345
+ path[0] === "source_sha256" &&
346
+ token[0][0] === '"'
347
+ ) {
348
+ const hash = JSON.parse(token[0]);
349
+ // Escaped values do not supply an unambiguous literal detector span.
350
+ if (token[0] !== JSON.stringify(hash))
351
+ throw new Error("Ambiguous digest spelling");
352
+ spans.push({
353
+ file: path[1],
354
+ hash,
355
+ start: Buffer.byteLength(text.slice(0, token.index + 1)),
356
+ end: Buffer.byteLength(
357
+ text.slice(0, token.index + token[0].length - 1)
358
+ ),
359
+ });
360
+ }
361
+ };
362
+ visit([], 0);
363
+ return cursor === tokens.length && spans.length === entries.length
364
+ ? { value, spans }
365
+ : null;
366
+ } catch {
367
+ // probe-direction: fail-closed — malformed, duplicate or over-budget JSON
368
+ // cannot establish evidence role; its detector findings remain blocking.
369
+ return null;
370
+ }
371
+ };
372
+
373
+ /** Clear only a uniquely attributed, independently established benign finding. */
374
+ const classifyEvidence = cwd => {
375
+ const budget = { remaining: EVIDENCE_BUDGET };
376
+ const blobs = new Map();
377
+ const maps = new Map();
378
+ const blob = (file, commit) => {
379
+ const key = `${commit}\0${file}`;
380
+ if (!blobs.has(key)) {
381
+ if (blobs.size >= 256) return null;
382
+ blobs.set(key, readBlob(file, commit, cwd, budget));
383
+ }
384
+ return blobs.get(key);
385
+ };
386
+ return row => {
387
+ if (
388
+ row.RuleID !== "generic-api-key" ||
389
+ !safePath(row.File) ||
390
+ row.SymlinkFile !== "" ||
391
+ row.Secret !== "REDACTED" ||
392
+ typeof row.Match !== "string" ||
393
+ row.EndLine !== row.StartLine ||
394
+ !Number.isSafeInteger(row.StartColumn) ||
395
+ !Number.isSafeInteger(row.EndColumn) ||
396
+ row.StartColumn < 1 ||
397
+ row.EndColumn < row.StartColumn
398
+ )
399
+ return false;
400
+ const bytes = blob(row.File, row.Commit);
401
+ if (!bytes) return false;
402
+ let lineStart = 0;
403
+ for (let line = 1; line < row.StartLine; line++) {
404
+ const next = bytes.indexOf(10, lineStart);
405
+ if (next < 0) return false;
406
+ lineStart = next + 1;
407
+ }
408
+ const newline = bytes.indexOf(10, lineStart);
409
+ const lineEnd = newline < 0 ? bytes.length : newline;
410
+ const replacements = row.Match.split("REDACTED");
411
+ if (replacements.length !== 2) return false;
412
+ const line = bytes.subarray(lineStart, lineEnd);
413
+ const attributed = (candidate, left, right) => {
414
+ const template = replacements.join(candidate);
415
+ if (template.split(candidate).length !== 2) return false;
416
+ const original = Buffer.from(template);
417
+ const offset = line.indexOf(original);
418
+ if (offset < 0 || line.indexOf(original, offset + 1) >= 0) return false;
419
+ const start = lineStart + offset;
420
+ const end = start + original.length;
421
+ if (
422
+ left !== start + Buffer.byteLength(replacements[0]) ||
423
+ right !== left + Buffer.byteLength(candidate)
424
+ )
425
+ return false;
426
+ // Pinned 8.30.1 detect/location.go counts a preceding fragment newline
427
+ // in columns after its first line. Prove exactly one of those two byte
428
+ // frames against the actual unique Git span; never infer fragment state.
429
+ return (
430
+ [0, 1].filter(
431
+ newlineByte =>
432
+ start === lineStart + row.StartColumn - 1 - newlineByte &&
433
+ end === lineStart + row.EndColumn - newlineByte
434
+ ).length === 1
435
+ );
436
+ };
437
+ // A complete standalone validation paragraph excludes surrounding JSON,
438
+ // assignments and interpolated/code contexts, not merely their filenames.
439
+ if (
440
+ (bytes.equals(Buffer.from(NARRATIVE)) ||
441
+ bytes.equals(Buffer.from(`${NARRATIVE}\n`))) &&
442
+ attributed(
443
+ TAXONOMY,
444
+ NARRATIVE.indexOf(TAXONOMY),
445
+ NARRATIVE.indexOf(TAXONOMY) + TAXONOMY.length
446
+ )
447
+ )
448
+ return true;
449
+ const key = `${row.Commit}\0${row.File}`;
450
+ if (!maps.has(key)) {
451
+ const parsed = evidenceMap(bytes);
452
+ let valid = parsed !== null;
453
+ const revision =
454
+ parsed && Object.hasOwn(parsed.value, "source_revision")
455
+ ? parsed.value.source_revision
456
+ : row.Commit;
457
+ if (parsed && Object.hasOwn(parsed.value, "source_revision")) {
458
+ valid =
459
+ typeof revision === "string" &&
460
+ new RegExp(`^[a-f0-9]{${row.Commit.length}}$`, "u").test(revision);
461
+ if (valid) {
462
+ const type = spawnSync(
463
+ "git",
464
+ [NO_REPLACEMENTS, "cat-file", "-t", revision],
465
+ {
466
+ cwd,
467
+ env: gitEnvironment(),
468
+ timeout: 120000,
469
+ maxBuffer: 1024 * 1024,
470
+ }
471
+ );
472
+ const ancestry = spawnSync(
473
+ "git",
474
+ [
475
+ NO_REPLACEMENTS,
476
+ "merge-base",
477
+ "--is-ancestor",
478
+ revision,
479
+ row.Commit,
480
+ ],
481
+ {
482
+ cwd,
483
+ env: gitEnvironment(),
484
+ timeout: 120000,
485
+ maxBuffer: 1024 * 1024,
486
+ }
487
+ );
488
+ // probe-direction: fail-closed — an absent/unreachable revision or
489
+ // failed ancestry probe cannot establish a historical preimage.
490
+ valid =
491
+ !type.error &&
492
+ !type.signal &&
493
+ type.status === 0 &&
494
+ type.stdout.toString() === "commit\n" &&
495
+ !type.stderr.length &&
496
+ !ancestry.error &&
497
+ !ancestry.signal &&
498
+ ancestry.status === 0 &&
499
+ !ancestry.stdout.length &&
500
+ !ancestry.stderr.length;
501
+ }
502
+ }
503
+ if (valid)
504
+ for (const span of parsed.spans) {
505
+ const sourceBytes = blob(span.file, revision);
506
+ if (!sourceBytes || digest(sourceBytes) !== span.hash) {
507
+ valid = false;
508
+ break;
509
+ }
510
+ }
511
+ maps.set(key, valid ? parsed.spans : []);
512
+ }
513
+ const candidates = maps
514
+ .get(key)
515
+ .filter(span => attributed(span.hash, span.start, span.end));
516
+ return candidates.length === 1;
517
+ };
518
+ };
519
+
225
520
  /** Only structured safe attribution is serialized, never stderr or metadata strings. */
226
521
  export const scanCommits = (commits, cwd, requested) => {
227
522
  const binary = qualifyScanner(requested);
@@ -263,6 +558,7 @@ export const scanCommits = (commits, cwd, requested) => {
263
558
  });
264
559
  const findings = [];
265
560
  const audit = auditedGit(scratch);
561
+ const benign = classifyEvidence(cwd);
266
562
  for (let offset = 0; offset < commits.length; offset += 100) {
267
563
  const group = commits.slice(offset, offset + 100);
268
564
  audit.begin();
@@ -334,11 +630,12 @@ export const scanCommits = (commits, cwd, requested) => {
334
630
  throw new HistorySecretError(
335
631
  "Scanner attribution is invalid. Repair pinned scanner execution; no raw report is published."
336
632
  );
337
- findings.push({
338
- rule: row.RuleID,
339
- commit: row.Commit,
340
- line: row.StartLine,
341
- });
633
+ if (!benign(row))
634
+ findings.push({
635
+ rule: row.RuleID,
636
+ commit: row.Commit,
637
+ line: row.StartLine,
638
+ });
342
639
  }
343
640
  }
344
641
  return {
@@ -1077,11 +1077,36 @@ function trackerContract(config = readConfig()) {
1077
1077
  );
1078
1078
  }
1079
1079
 
1080
+ /**
1081
+ * Resolve authored references before any binding or provider decision.
1082
+ *
1083
+ * The literal `github/` namespace is an identity spelling, not a different
1084
+ * tracker or an exemption. Its ASCII segments must survive unchanged; URL
1085
+ * parsing or path normalization would incorrectly admit encoded/dot paths.
1086
+ * @param {unknown} raw Authored work-item reference.
1087
+ * @param {object} contract Existing configured tracker contract.
1088
+ * @returns {string} Repository-scoped canonical identity.
1089
+ */
1080
1090
  function canonicalizeRef(raw, contract = trackerContract()) {
1081
1091
  const value = String(raw ?? "").trim();
1082
1092
  if (contract.provider === "github") {
1083
- const match = /^([^\s/#]+\/[^\s/#]+)#([1-9]\d*)$/.exec(value);
1084
- if (!match)
1093
+ const qualified =
1094
+ /^github\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9]\d*)$/.exec(value);
1095
+ // Match authored URL bytes before normalization: URL parsers can erase a
1096
+ // port or decode a path that this input contract deliberately refuses.
1097
+ const match =
1098
+ qualified ??
1099
+ /^([^\s/#]+\/[^\s/#]+)#([1-9]\d*)$/.exec(value) ??
1100
+ /^https:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/issues\/([1-9]\d*)$/.exec(
1101
+ value
1102
+ );
1103
+ if (
1104
+ !match ||
1105
+ (qualified &&
1106
+ qualified[1]
1107
+ .split("/")
1108
+ .some(segment => segment === "." || segment === ".."))
1109
+ )
1085
1110
  throw new TrackingError(
1086
1111
  `Invalid GitHub Work-Item '${value}'; expected owner/repo#123`
1087
1112
  );
@@ -1438,6 +1463,18 @@ export function soleWorkItem(text, contract, subject) {
1438
1463
  const refs = [
1439
1464
  ...new Set(values.map(value => canonicalizeRef(value, contract))),
1440
1465
  ];
1466
+ // Canonical repeats (including repository case variants) retain their
1467
+ // meaning. Mixing the URL and canonical representations is two authored
1468
+ // declarations even when normalization would collapse their identity.
1469
+ if (
1470
+ contract.provider === "github" &&
1471
+ values.some(value => value.startsWith("https://github.com/")) &&
1472
+ values.some(value => !value.startsWith("https://github.com/"))
1473
+ ) {
1474
+ throw new TrackingError(
1475
+ `${subject[0].toUpperCase()}${subject.slice(1)} mixes URL and canonical Work-Item declarations; it must name exactly one`
1476
+ );
1477
+ }
1441
1478
  if (refs.length > 1) {
1442
1479
  throw new TrackingError(
1443
1480
  `${subject[0].toUpperCase()}${subject.slice(1)} names ${refs.length} different work items (${refs.join(", ")}); it must name exactly one`
@@ -3526,7 +3563,24 @@ function prWorkItems(body, contract) {
3526
3563
  throw new TrackingError(
3527
3564
  "No Work-Item trailer anywhere in the pull request body"
3528
3565
  );
3529
- return [...new Set(values.map(value => canonicalizeRef(value, contract)))];
3566
+ const declared = values.map(value => ({
3567
+ value,
3568
+ ref: canonicalizeRef(value, contract),
3569
+ }));
3570
+ // Validation precedes representation checks; canonical GitHub refs lack ://.
3571
+ const urlRefs = new Set(
3572
+ declared.filter(({ value }) => value.includes("://")).map(({ ref }) => ref)
3573
+ );
3574
+ if (
3575
+ contract.provider === "github" &&
3576
+ declared.some(
3577
+ ({ value, ref }) => !value.includes("://") && urlRefs.has(ref)
3578
+ )
3579
+ )
3580
+ throw new TrackingError(
3581
+ "Pull request body mixes URL and canonical Work-Item declarations for the same work item"
3582
+ );
3583
+ return [...new Set(declared.map(({ ref }) => ref))];
3530
3584
  }
3531
3585
 
3532
3586
  /**
@@ -4507,6 +4561,10 @@ function backlink(args) {
4507
4561
  * repository does the commit declare? — and a value the filter drops is one the
4508
4562
  * reader still saw. That ordering is what keeps a malformed trailer refusable
4509
4563
  * by the gate instead of invisible to it.
4564
+ * Qualified `github/owner/name#number` values use the SAME canonical resolver
4565
+ * as the gates, so the audit does not lose attribution accepted by a hook.
4566
+ * This audit still requires a hash-number declaration; supported issue URLs
4567
+ * remain a gate input, not a newly admitted audit declaration.
4510
4568
  *
4511
4569
  * CRLF needs nothing here, and a relayed review finding that said otherwise
4512
4570
  * was refuted rather than acted on. `workItemLines` splits on `/\r?\n/`, so a
@@ -4518,16 +4576,19 @@ function backlink(args) {
4518
4576
  * @returns {number[]} Declared issue numbers, de-duplicated, in first-seen order.
4519
4577
  */
4520
4578
  export function declaredWorkItemNumbers(body, repository) {
4521
- const owner = String(repository ?? "").toLowerCase();
4579
+ const owner = String(repository ?? "");
4522
4580
  if (owner === "") return [];
4581
+ const contract = { provider: "github", repository: owner };
4523
4582
  const numbers = [];
4524
4583
  for (const value of workItemLines(body)) {
4525
- const hash = value.indexOf("#");
4526
- if (hash === -1) continue;
4527
- if (value.slice(0, hash).toLowerCase() !== owner) continue;
4528
- const digits = value.slice(hash + 1);
4529
- if (!isIssueNumber(digits)) continue;
4530
- numbers.push(Number(digits));
4584
+ if (!value.includes("#")) continue;
4585
+ try {
4586
+ const ref = canonicalizeRef(value, contract);
4587
+ const digits = ref.slice(ref.indexOf("#") + 1);
4588
+ if (isIssueNumber(digits)) numbers.push(Number(digits));
4589
+ } catch (error) {
4590
+ if (!(error instanceof TrackingError)) throw error;
4591
+ }
4531
4592
  }
4532
4593
  return [...new Set(numbers)];
4533
4594
  }
@@ -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,CAi7CjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAo1CjC,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,CAy7CjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA41CjC,CAAC"}
@@ -456,8 +456,11 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
456
456
  "ca75ebbc45fca4c52400612ae818d231a28289b50a70f6cdee6feb51b144896f",
457
457
  ]),
458
458
  "scripts/lib/history-secret-scanner.mjs": Object.freeze([
459
+ "223cc0e59b19b60e0e78a8a85710cab634ef90cd4661b5a5d608455afbf9c4c4",
459
460
  "501452c19be923616a6ea1ed9443c86cbac14e4aa3073e579f6af82d1423a2c9",
460
461
  "6afcb5ea42d8f9046a17e2253d4ddbefdf9025b150870afda23db77796f05f25",
462
+ "a68a4ba784699c470e1525ed3cd2f31f399cc80472d2bf5653d936bf3252c658",
463
+ "fc2677b9f5ae86ee9709bb59f1e9cf231f258d12ddcfbf9a6dd9a711155220d2",
461
464
  "ff90535b7c6f24f48651ab93d204a1530c6bfaa8e6169d3aa83ab069c1a1ef22",
462
465
  ]),
463
466
  "scripts/lib/invoked-as-script.mjs": Object.freeze([
@@ -1224,6 +1227,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
1224
1227
  "04f78df45465d01601c8b5711b74749b2a2c2b70b9c916d6790bd69e1c11865d",
1225
1228
  "06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
1226
1229
  "08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
1230
+ "0a9d388ac78181e23aa4a23d6b2e9af04b30f204f39a0378a5bbd60612d907ce",
1227
1231
  "0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
1228
1232
  "0fdec8bbfb55e97e9caa91441c8306b0f7d0b9a8957b80ce68fbe98346e0d63e",
1229
1233
  "10b9e0e2db549609fb4ab66ebe9b4f755bf7f6cc6be606f1a3515f5ebe6d5110",
@@ -1239,6 +1243,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
1239
1243
  "23b58ea56cb26fbdef204d7bdbc4f18fe097947d64f733381823e900a30ea70c",
1240
1244
  "250d44a7a862e59cbbc35f66e8dd28e68c9a9df7cd1c27c547b3aadadc2b5956",
1241
1245
  "275b4d773ecefeeceee972191086cffaa159a4cb3cac2dae3d85dfd363c69fb6",
1246
+ "2a7528b1362a5e73c01874d5dd915f030f023c12621bfe87830295480baaa8e8",
1242
1247
  "2b7e81f0e23628da9c3a124f3d5b0593e3a8c564cd595329b9f37510540a5581",
1243
1248
  "2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
1244
1249
  "2cd876ccf562b8c2b146817f80ebc189f9e31989457067c14938ed5e745a94b9",
@@ -1255,6 +1260,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
1255
1260
  "42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
1256
1261
  "4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
1257
1262
  "475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
1263
+ "47677cf8b388e62fc82efdf564f94a784499f23b7f290ee1d7f9f50cc8a8abc3",
1258
1264
  "4ae4e4e25c5fc075824a112a26abae6ab157c2888332c376db928b51530abc0a",
1259
1265
  "4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
1260
1266
  "4d054e3b7a266d69108f7cec12ee02e6a302670ff5932dc398cb0c5069b2a44f",
@@ -1282,6 +1288,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
1282
1288
  "6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
1283
1289
  "6ced6d3929f58e0b0b3e12717af537f9c4f57ad03c18d99e774026c169b13874",
1284
1290
  "6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
1291
+ "6ec811517f212f6e6b33a8163f16a6b0538731c9cbe626e9f62a55b0f0af9518",
1285
1292
  "7156449f0ad8c68c40c3f7f02df91248e32d9d83a439b38c4ce0a3fd35e04c1e",
1286
1293
  "71fcbf510d3ac112417cea1bd0b4fd4b478e7f01a51f04b6a28fb7a487443d36",
1287
1294
  "71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
@@ -1345,6 +1352,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
1345
1352
  "e41e9534988ce69a836664437a78f3a3f3dc7ebbe396c86cdab0ea63fb7c19da",
1346
1353
  "e5e7d4322195d0e675fd62a8630cefa811d2459cea752163787d01cfa65e5ed7",
1347
1354
  "e80c460034868c7e4152dd4276881105f80b3b76f4f130bff1ab662e17a9cb5a",
1355
+ "e94675ad68dd4254ad96fb1f2f67eb58447d4eb4f04475200d449f4932c78824",
1348
1356
  "ea63f5b9e02ed081e21bebd928e77b3f7315addd2b8001112cb423eea459cd5f",
1349
1357
  "ea82b8b47c8367fc732ed8c70ba2cbf882aa01b40e40d40a95c5874044206815",
1350
1358
  "ec2aa4c645fb1573aa2dfb731e7ced02a00b7f56a3eb853157ac7e74c0a04f92",
@@ -1907,8 +1915,11 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
1907
1915
  "ca75ebbc45fca4c52400612ae818d231a28289b50a70f6cdee6feb51b144896f",
1908
1916
  ]),
1909
1917
  "scripts/lib/history-secret-scanner.mjs": Object.freeze([
1918
+ "223cc0e59b19b60e0e78a8a85710cab634ef90cd4661b5a5d608455afbf9c4c4",
1910
1919
  "501452c19be923616a6ea1ed9443c86cbac14e4aa3073e579f6af82d1423a2c9",
1911
1920
  "6afcb5ea42d8f9046a17e2253d4ddbefdf9025b150870afda23db77796f05f25",
1921
+ "a68a4ba784699c470e1525ed3cd2f31f399cc80472d2bf5653d936bf3252c658",
1922
+ "fc2677b9f5ae86ee9709bb59f1e9cf231f258d12ddcfbf9a6dd9a711155220d2",
1912
1923
  "ff90535b7c6f24f48651ab93d204a1530c6bfaa8e6169d3aa83ab069c1a1ef22",
1913
1924
  ]),
1914
1925
  "scripts/lib/invoked-as-script.mjs": Object.freeze([
@@ -2607,6 +2618,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
2607
2618
  "04401ae05bf9464e6929180ffdfff80afc528a1df20733b032a0342252ca1d4a",
2608
2619
  "06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
2609
2620
  "08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
2621
+ "0a9d388ac78181e23aa4a23d6b2e9af04b30f204f39a0378a5bbd60612d907ce",
2610
2622
  "0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
2611
2623
  "0fdec8bbfb55e97e9caa91441c8306b0f7d0b9a8957b80ce68fbe98346e0d63e",
2612
2624
  "10b9e0e2db549609fb4ab66ebe9b4f755bf7f6cc6be606f1a3515f5ebe6d5110",
@@ -2622,6 +2634,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
2622
2634
  "23b58ea56cb26fbdef204d7bdbc4f18fe097947d64f733381823e900a30ea70c",
2623
2635
  "250d44a7a862e59cbbc35f66e8dd28e68c9a9df7cd1c27c547b3aadadc2b5956",
2624
2636
  "275b4d773ecefeeceee972191086cffaa159a4cb3cac2dae3d85dfd363c69fb6",
2637
+ "2a7528b1362a5e73c01874d5dd915f030f023c12621bfe87830295480baaa8e8",
2625
2638
  "2b7e81f0e23628da9c3a124f3d5b0593e3a8c564cd595329b9f37510540a5581",
2626
2639
  "2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
2627
2640
  "2cd876ccf562b8c2b146817f80ebc189f9e31989457067c14938ed5e745a94b9",
@@ -2638,6 +2651,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
2638
2651
  "42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
2639
2652
  "4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
2640
2653
  "475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
2654
+ "47677cf8b388e62fc82efdf564f94a784499f23b7f290ee1d7f9f50cc8a8abc3",
2641
2655
  "4ae4e4e25c5fc075824a112a26abae6ab157c2888332c376db928b51530abc0a",
2642
2656
  "4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
2643
2657
  "4d054e3b7a266d69108f7cec12ee02e6a302670ff5932dc398cb0c5069b2a44f",
@@ -2664,6 +2678,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
2664
2678
  "6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
2665
2679
  "6ced6d3929f58e0b0b3e12717af537f9c4f57ad03c18d99e774026c169b13874",
2666
2680
  "6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
2681
+ "6ec811517f212f6e6b33a8163f16a6b0538731c9cbe626e9f62a55b0f0af9518",
2667
2682
  "7156449f0ad8c68c40c3f7f02df91248e32d9d83a439b38c4ce0a3fd35e04c1e",
2668
2683
  "71fcbf510d3ac112417cea1bd0b4fd4b478e7f01a51f04b6a28fb7a487443d36",
2669
2684
  "71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
@@ -2726,6 +2741,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
2726
2741
  "e41e9534988ce69a836664437a78f3a3f3dc7ebbe396c86cdab0ea63fb7c19da",
2727
2742
  "e5e7d4322195d0e675fd62a8630cefa811d2459cea752163787d01cfa65e5ed7",
2728
2743
  "e80c460034868c7e4152dd4276881105f80b3b76f4f130bff1ab662e17a9cb5a",
2744
+ "e94675ad68dd4254ad96fb1f2f67eb58447d4eb4f04475200d449f4932c78824",
2729
2745
  "ea63f5b9e02ed081e21bebd928e77b3f7315addd2b8001112cb423eea459cd5f",
2730
2746
  "ea82b8b47c8367fc732ed8c70ba2cbf882aa01b40e40d40a95c5874044206815",
2731
2747
  "ec2aa4c645fb1573aa2dfb731e7ced02a00b7f56a3eb853157ac7e74c0a04f92",