@codyswann/lisa 4.72.6 → 4.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +19 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +4 -2
  3. package/all/copy-overwrite/scripts/lib/history-secret-evidence-shape.mjs +398 -6
  4. package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +175 -7
  5. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +7 -1
  6. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +11 -66
  7. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +3 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +10 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +162 -121
  10. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +8 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-hosted-scope.mjs +177 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-policy.mjs +73 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +1 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-daemon.mjs +227 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-resource.mjs +176 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-storage.mjs +201 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql.mjs +220 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-rails-runtime-contract.mjs +75 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-downloads.mjs +187 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-identity.mjs +288 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-rails-tools.mjs +292 -0
  22. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +3 -0
  23. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +1 -1
  24. package/all/copy-overwrite/scripts/lisa-scratch-run.sh +7 -4
  25. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +148 -20
  26. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.js +126 -0
  28. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  29. package/dist/core/lisa.d.ts.map +1 -1
  30. package/dist/core/lisa.js +2 -1
  31. package/dist/core/lisa.js.map +1 -1
  32. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  33. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  34. package/dist/core/upstream-evidence-manifest.js +79 -17
  35. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  36. package/dist/opencode/plugin-templates/parity-safety-net.sh +1 -1
  37. package/package.json +4 -4
  38. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa/hooks/parity-safety-net.sh +1 -1
  41. package/plugins/lisa-agy/hooks/parity-safety-net.sh +1 -1
  42. package/plugins/lisa-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  46. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +1 -1
  50. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +1 -1
  52. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-agy/plugin.json +1 -1
  55. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  75. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-agy/plugin.json +1 -1
  80. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  85. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  90. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/src/base/hooks/parity-safety-net.sh +1 -1
  93. package/scripts/two-channel-couplings.json +1 -1
@@ -36,6 +36,25 @@ export function optionalLockFields(value) {
36
36
  return ["bunLockSha256"];
37
37
  }
38
38
 
39
+ /** Optional runtime authority is a digest, never an unsigned executable profile. */
40
+ export function optionalRuntimeFields(value) {
41
+ if (!Object.hasOwn(value, "runtimeSha256")) return [];
42
+ requireProof(
43
+ typeof value.runtimeSha256 === "string" && HEX.test(value.runtimeSha256),
44
+ "invalid original Rails runtime digest"
45
+ );
46
+ return ["runtimeSha256"];
47
+ }
48
+
49
+ /** One exact optional projection preserves historical keys at producer and verifier boundaries. */
50
+ export function signedProposalFields(value) {
51
+ return Object.fromEntries(
52
+ [...optionalLockFields(value), ...optionalRuntimeFields(value)].map(
53
+ name => [name, value[name]]
54
+ )
55
+ );
56
+ }
57
+
39
58
  /** Signed original presence selects one closed two- or three-file cohort. */
40
59
  export function proposalFileNames(value) {
41
60
  const files = optionalLockFields(value).length
@@ -14,11 +14,12 @@ import {
14
14
  canonicalJson,
15
15
  exactKeys,
16
16
  proposalFileNames,
17
- optionalLockFields,
17
+ signedProposalFields,
18
18
  DESCRIPTOR_KEYS,
19
19
  HEX,
20
20
  OBJECT_ID,
21
21
  } from "./automation-provenance-contract.mjs";
22
+ import { assertCommittedRuntime } from "./npm-update-rails-runtime-contract.mjs";
22
23
 
23
24
  /** Git reads always use the actual checkout and bounded shared runner. */
24
25
  export function git(args) {
@@ -98,7 +99,7 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
98
99
  }).decode(messageBytes);
99
100
  exactKeys(
100
101
  descriptor,
101
- [...DESCRIPTOR_KEYS, ...optionalLockFields(descriptor)],
102
+ [...DESCRIPTOR_KEYS, ...Object.keys(signedProposalFields(descriptor))],
102
103
  "descriptor"
103
104
  );
104
105
  requireProof(
@@ -142,6 +143,7 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
142
143
  descriptor.parent === git(["rev-parse", "HEAD"]),
143
144
  "proposal parent differs"
144
145
  );
146
+ assertCommittedRuntime(descriptor, git);
145
147
  requireProof(
146
148
  OBJECT_ID.test(descriptor.tree) && descriptor.tree === git(["write-tree"]),
147
149
  "final staged tree differs"
@@ -1,6 +1,9 @@
1
1
  // This file is managed by Lisa and IS replaced on each `lisa` run.
2
2
  // Do not edit directly — durable changes belong upstream in Lisa.
3
3
  /** Bounded lossless evidence syntax; no digest is authenticated by syntax alone. */
4
+ const SOURCE_MAP_FORMAT = "json-source-map";
5
+ const PROOF_MAP_FORMAT = "json-proof-map";
6
+ const TAXONOMY = "Disk/missing/corrupt";
4
7
  const credentialField =
5
8
  /^(?:api[_-]?key|access[_-]?token|password|passwd|secret)$/iu;
6
9
  const credentialToken =
@@ -34,15 +37,18 @@ const isHash = value =>
34
37
 
35
38
  /** Native validation and duplicate-aware lexical spans share the exact same text. */
36
39
  const lexical = text => {
40
+ const tokens = [];
41
+ for (const token of text.matchAll(
42
+ /"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^{}[\],:\s]+/gu
43
+ )) {
44
+ if (tokens.length >= 8192) throw new Error("Bounded JSON tokens");
45
+ tokens.push(token);
46
+ }
37
47
  const value = JSON.parse(text);
38
- const tokens = [
39
- ...text.matchAll(/"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^{}[\],:\s]+/gu),
40
- ];
41
- if (tokens.length > 8192) throw new Error("Bounded JSON tokens");
42
48
  const state = { cursor: 0, spans: [] };
43
49
  visit(tokens, state, [], 0, text);
44
50
  if (state.cursor !== tokens.length) throw new Error("Trailing JSON tokens");
45
- return { value, spans: state.spans };
51
+ return { value, spans: state.spans, tokens };
46
52
  };
47
53
 
48
54
  /** Duplicate decoded keys and escaped attribution spellings always fail closed. */
@@ -161,6 +167,392 @@ export const evidenceMaps = bytes => {
161
167
  return result;
162
168
  };
163
169
 
170
+ const coordinateFormats = new Map([
171
+ [
172
+ SOURCE_MAP_FORMAT,
173
+ new Set([
174
+ "/selected_artifact_hashes",
175
+ "/source_hashes",
176
+ "/source_hashes_after",
177
+ ]),
178
+ ],
179
+ ["json-root-map", new Set([""])],
180
+ ["markdown-json-files", new Set(["/files"])],
181
+ [PROOF_MAP_FORMAT, new Set(["/proof_sha256"])],
182
+ ]);
183
+ const record = value =>
184
+ value !== null && typeof value === "object" && !Array.isArray(value);
185
+ const fieldsExactly = (value, fields) =>
186
+ record(value) &&
187
+ Object.keys(value).length === fields.length &&
188
+ fields.every(field => Object.hasOwn(value, field));
189
+ const objectId = (value, width) =>
190
+ typeof value === "string" &&
191
+ [40, 64].includes(width) &&
192
+ new RegExp(`^[a-f0-9]{${width}}$`, "u").test(value);
193
+ const documentText = bytes => {
194
+ if (bytes.length > 1024 * 1024) return null;
195
+ const text = bytes.toString("utf8");
196
+ return Buffer.from(text).equals(bytes) ? text : null;
197
+ };
198
+ const coordinateOrigin = (origin, width) =>
199
+ fieldsExactly(origin, [
200
+ "commit",
201
+ "blob",
202
+ "path",
203
+ "format",
204
+ "map_pointer",
205
+ "value_span",
206
+ ]) &&
207
+ objectId(origin.commit, width) &&
208
+ objectId(origin.blob, width) &&
209
+ safeSourcePath(origin.path) &&
210
+ coordinateFormats.get(origin.format)?.has(origin.map_pointer) === true &&
211
+ Array.isArray(origin.value_span) &&
212
+ origin.value_span.length === 2 &&
213
+ origin.value_span.every(
214
+ value => Number.isSafeInteger(value) && value >= 0 && value <= 1024 * 1024
215
+ ) &&
216
+ origin.value_span[1] - origin.value_span[0] === 64;
217
+ const coordinatePreimage = (preimage, width) => {
218
+ if (preimage?.kind === "source")
219
+ return Object.hasOwn(preimage, "commit")
220
+ ? fieldsExactly(preimage, ["kind", "commit"]) &&
221
+ objectId(preimage.commit, width)
222
+ : fieldsExactly(preimage, ["kind"]);
223
+ return (
224
+ fieldsExactly(preimage, ["kind", "commit", "path"]) &&
225
+ preimage.kind === "archive" &&
226
+ objectId(preimage.commit, width) &&
227
+ preimage.path === "evidence/archive/neutral-artifact.json"
228
+ );
229
+ };
230
+
231
+ /** Closed version1 coordinates carry no digest, opaque original key or caller role. */
232
+ export const coordinateManifest = (bytes, width) => {
233
+ const text = documentText(bytes);
234
+ if (text === null) return null;
235
+ try {
236
+ const parsed = lexical(text);
237
+ if (
238
+ parsed.tokens.some(
239
+ token =>
240
+ token[0].startsWith('"') &&
241
+ token[0] !== JSON.stringify(JSON.parse(token[0]))
242
+ )
243
+ )
244
+ return null;
245
+ const value = parsed.value;
246
+ if (
247
+ !fieldsExactly(value, ["version", "entries"]) ||
248
+ value.version !== 1 ||
249
+ !Array.isArray(value.entries) ||
250
+ value.entries.length > 256
251
+ )
252
+ return null;
253
+ if (
254
+ value.entries.some(
255
+ entry =>
256
+ !fieldsExactly(entry, ["origin", "preimage"]) ||
257
+ !coordinateOrigin(entry.origin, width) ||
258
+ !coordinatePreimage(entry.preimage, width)
259
+ )
260
+ )
261
+ return null;
262
+ return value.entries;
263
+ } catch {
264
+ return null;
265
+ }
266
+ };
267
+
268
+ /** Scan maximal opening markers without backtracking across a long fence line. */
269
+ const openingFence = line => {
270
+ const state = { start: 0, end: 0 };
271
+ while (state.start < 3 && line[state.start] === " ") state.start++;
272
+ state.end = state.start;
273
+ const marker = line[state.start];
274
+ if (marker !== "`" && marker !== "~") return null;
275
+ while (line[state.end] === marker) state.end++;
276
+ const info = line.slice(state.end);
277
+ return state.end - state.start >= 3 && !/[\r\n]/u.test(info)
278
+ ? [line.slice(state.start, state.end), info]
279
+ : null;
280
+ };
281
+
282
+ /** Strip only trailing CR/LF bytes, with a single bounded backwards pass. */
283
+ const withoutLineEnding = line => {
284
+ const state = { end: line.length };
285
+ while (state.end > 0 && /[\r\n]/u.test(line[state.end - 1])) state.end--;
286
+ return line.slice(0, state.end);
287
+ };
288
+
289
+ /** Track actual Markdown fences, including nested-looking text inside another fence. */
290
+ const markdownFences = text => {
291
+ const result = [];
292
+ let active = null;
293
+ let offset = 0;
294
+ for (const line of text.split(/(?<=\n)/u)) {
295
+ const clean = withoutLineEnding(line);
296
+ if (active) {
297
+ const close = clean.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/u);
298
+ if (
299
+ close &&
300
+ close[1][0] === active.marker[0] &&
301
+ close[1].length >= active.marker.length
302
+ ) {
303
+ result.push({
304
+ ...active,
305
+ bodyEnd: offset,
306
+ end: offset + line.length,
307
+ closed: true,
308
+ });
309
+ active = null;
310
+ }
311
+ } else {
312
+ const open = openingFence(clean);
313
+ if (open && !(open[0][0] === "`" && open[1].includes("`")))
314
+ active = {
315
+ start: offset,
316
+ marker: open[0],
317
+ info: open[1].trim(),
318
+ bodyStart: offset + line.length,
319
+ };
320
+ }
321
+ offset += line.length;
322
+ }
323
+ if (active)
324
+ result.push({
325
+ ...active,
326
+ bodyEnd: text.length,
327
+ end: text.length,
328
+ closed: false,
329
+ });
330
+ return result;
331
+ };
332
+ const markdownTokensWithinBudget = text => {
333
+ let count = 0;
334
+ for (const _token of text.matchAll(
335
+ /"(?:\\[\s\S]|[^"\\])*"|[`~]+|[\p{L}\p{N}]+|[^\s\p{L}\p{N}]/gu
336
+ ))
337
+ if (++count > 8192) return false;
338
+ return true;
339
+ };
340
+ const completeMap = (value, isPath) =>
341
+ record(value) &&
342
+ Object.keys(value).length > 0 &&
343
+ Object.keys(value).length <= 256 &&
344
+ Object.entries(value).every(
345
+ ([key, literal]) => isPath(key) && isHash(literal)
346
+ );
347
+ const opaqueKey = key => typeof key === "string" && key.length > 0;
348
+ const coordinateMap = (parsed, role, format, offset = 0) => {
349
+ const value = role === null ? parsed.value : parsed.value[role];
350
+ if (
351
+ !completeMap(
352
+ value,
353
+ format === PROOF_MAP_FORMAT ? opaqueKey : safeSourcePath
354
+ )
355
+ )
356
+ return null;
357
+ const spans = parsed.spans
358
+ .filter(span =>
359
+ role === null
360
+ ? span.path.length === 1
361
+ : span.path.length === 2 && span.path[0] === role
362
+ )
363
+ .map(span => ({
364
+ file: span.path.at(-1),
365
+ hash: span.hash,
366
+ start: span.start + offset,
367
+ end: span.end + offset,
368
+ }));
369
+ if (spans.length !== Object.keys(value).length) return null;
370
+ return {
371
+ kind: format === PROOF_MAP_FORMAT ? "proof" : "source",
372
+ format,
373
+ pointer: role === null ? "" : `/${role}`,
374
+ value: parsed.value,
375
+ spans,
376
+ };
377
+ };
378
+ const coordinateJsonMaps = parsed => {
379
+ if (!record(parsed.value)) return [];
380
+ if (completeMap(parsed.value, safeSourcePath))
381
+ return [coordinateMap(parsed, null, "json-root-map")];
382
+ const roles = [
383
+ "selected_artifact_hashes",
384
+ "source_hashes",
385
+ "source_hashes_after",
386
+ "proof_sha256",
387
+ ].filter(role => Object.hasOwn(parsed.value, role));
388
+ const result = roles.map(role =>
389
+ coordinateMap(
390
+ parsed,
391
+ role,
392
+ role === "proof_sha256" ? PROOF_MAP_FORMAT : SOURCE_MAP_FORMAT
393
+ )
394
+ );
395
+ return result.some(map => map === null) ? [] : result;
396
+ };
397
+
398
+ /** Complete documents derive exact fixed map roles independently of manifest assertions. */
399
+ export const coordinateMaps = bytes => {
400
+ const text = documentText(bytes);
401
+ if (text === null) return [];
402
+ try {
403
+ return coordinateJsonMaps(lexical(text));
404
+ } catch {
405
+ /* A complete Markdown document may supply one independently parsed JSON fence. */
406
+ }
407
+ if (!markdownTokensWithinBudget(text)) return [];
408
+ const blocks = markdownFences(text).filter(block => block.info === "json");
409
+ if (blocks.length !== 1 || !blocks[0].closed) return [];
410
+ try {
411
+ const block = blocks[0];
412
+ const parsed = lexical(text.slice(block.bodyStart, block.bodyEnd));
413
+ if (
414
+ !record(parsed.value) ||
415
+ !completeMap(parsed.value.files, safeSourcePath) ||
416
+ !completeMap(parsed.value.logs, opaqueKey)
417
+ )
418
+ return [];
419
+ const map = coordinateMap(
420
+ parsed,
421
+ "files",
422
+ "markdown-json-files",
423
+ Buffer.byteLength(text.slice(0, block.bodyStart))
424
+ );
425
+ return map ? [map] : [];
426
+ } catch {
427
+ return [];
428
+ }
429
+ };
430
+
431
+ const paragraphBreak = line =>
432
+ /^\s*$/u.test(line) ||
433
+ /^ {0,3}(?:>|#{1,6}[ \t]|`{3,}|~{3,}|(?:[-*+] |\d+[.)] ))/u.test(line) ||
434
+ /^(?: {4}|\t)/u.test(line);
435
+ const paragraphAt = (text, position) => {
436
+ const lines = [];
437
+ let start = 0;
438
+ for (const textLine of text.split(/(?<=\n)/u)) {
439
+ lines.push({ text: textLine, start, end: start + textLine.length });
440
+ start += textLine.length;
441
+ }
442
+ const index = lines.findIndex(
443
+ line => position >= line.start && position < line.end
444
+ );
445
+ if (index < 0) return null;
446
+ let first = index;
447
+ let last = index;
448
+ while (first > 0 && !paragraphBreak(lines[first].text)) {
449
+ if (/^\s*$/u.test(lines[first - 1].text)) break;
450
+ if (paragraphBreak(lines[first - 1].text)) {
451
+ if (/^ {0,3}>/u.test(lines[first - 1].text)) return null;
452
+ if (/^ {0,3}[-*+] /u.test(lines[first - 1].text)) first--;
453
+ break;
454
+ }
455
+ first--;
456
+ }
457
+ while (last + 1 < lines.length && !paragraphBreak(lines[last + 1].text))
458
+ last++;
459
+ return {
460
+ start: lines[first].start,
461
+ text: text.slice(lines[first].start, lines[last].end),
462
+ };
463
+ };
464
+ const inlineCode = text => {
465
+ const ranges = [];
466
+ const runs = [...text.matchAll(/`+/gu)];
467
+ let index = 0;
468
+ while (index < runs.length) {
469
+ const close = runs.findIndex(
470
+ (run, candidate) =>
471
+ candidate > index && run[0].length === runs[index][0].length
472
+ );
473
+ if (close < 0) return null;
474
+ ranges.push([runs[index].index, runs[close].index + runs[close][0].length]);
475
+ index = close + 1;
476
+ }
477
+ return ranges;
478
+ };
479
+ const quotedRanges = text => {
480
+ const ranges = [];
481
+ let quote = null;
482
+ let start = 0;
483
+ for (let index = 0; index < text.length; index++) {
484
+ const char = text[index];
485
+ if (char !== "'" && char !== '"') continue;
486
+ if (quote === char) {
487
+ ranges.push([start, index + 1]);
488
+ quote = null;
489
+ } else if (quote === null) {
490
+ if (char === "'" && /[\p{L}\p{N}]/u.test(text[index - 1] ?? "")) continue;
491
+ quote = char;
492
+ start = index;
493
+ }
494
+ }
495
+ return quote === null ? ranges : null;
496
+ };
497
+
498
+ /** One exact taxonomy occurrence may live inside a complete larger Markdown paragraph. */
499
+ export const paragraphNarrativeSpan = (bytes, lineStart, lineEnd) => {
500
+ const text = documentText(bytes);
501
+ if (text === null || !markdownTokensWithinBudget(text)) return null;
502
+ const position = bytes.subarray(0, lineStart).toString("utf8").length;
503
+ if (
504
+ markdownFences(text).some(
505
+ block => position >= block.start && position < block.end
506
+ )
507
+ )
508
+ return null;
509
+ const paragraph = paragraphAt(text, position);
510
+ if (!paragraph) return null;
511
+ const phrase = `AccessDenied boundaries, ${TAXONOMY} falsifications`;
512
+ const index = paragraph.text.indexOf(phrase);
513
+ if (
514
+ index < 0 ||
515
+ paragraph.text.indexOf(phrase, index + 1) >= 0 ||
516
+ !/[,.]/u.test(paragraph.text[index + phrase.length] ?? "")
517
+ )
518
+ return null;
519
+ const ranges = inlineCode(paragraph.text);
520
+ if (
521
+ ranges === null ||
522
+ ranges.some(([start, end]) => index < end && index + phrase.length > start)
523
+ )
524
+ return null;
525
+ let prose = paragraph.text;
526
+ for (const [start, end] of [...ranges].reverse())
527
+ prose = `${prose.slice(0, start)}identifier${prose.slice(end)}`;
528
+ const quotes = quotedRanges(prose);
529
+ const proseIndex = prose.indexOf(phrase);
530
+ if (
531
+ quotes === null ||
532
+ quotes.some(
533
+ ([start, end]) => proseIndex < end && proseIndex + phrase.length > start
534
+ )
535
+ )
536
+ return null;
537
+ if (
538
+ !/^(?: {0,3}[-*+] )?\p{L}[\p{L}\p{N}\s,./():;!?"'-]*[.!?]\s*$/u.test(prose)
539
+ )
540
+ return null;
541
+ if (
542
+ [...prose.matchAll(/(?:^|\s)([a-z_-]+)\s*:/giu)].some(
543
+ match => credentialField.test(match[1]) || credentialToken.test(match[1])
544
+ )
545
+ )
546
+ return null;
547
+ const start = Buffer.byteLength(
548
+ text.slice(0, paragraph.start + index + phrase.indexOf(TAXONOMY))
549
+ );
550
+ const end = start + TAXONOMY.length;
551
+ return start >= lineStart && end <= lineEnd
552
+ ? { hash: TAXONOMY, start, end }
553
+ : null;
554
+ };
555
+
164
556
  /** Only the fixed taxonomy in ordinary prose, outside Markdown code, is a candidate. */
165
557
  export const narrativeSpan = (bytes, lineStart, lineEnd) => {
166
558
  const prior = bytes.subarray(0, lineStart).toString("utf8");
@@ -172,7 +564,7 @@ export const narrativeSpan = (bytes, lineStart, lineEnd) => {
172
564
  const line = bytes.subarray(lineStart, lineEnd).toString("utf8");
173
565
  const prose = line.replace(/`[^`\r\n]{1,256}`/gu, "identifier");
174
566
  if (!/^(?:[-*] )?[A-Za-z][A-Za-z0-9 ,./():-]*\.$/u.test(prose)) return null;
175
- const taxonomy = "Disk/missing/corrupt";
567
+ const taxonomy = TAXONOMY;
176
568
  const phrase = `AccessDenied boundaries, ${taxonomy} falsifications`;
177
569
  const index = line.indexOf(phrase);
178
570
  if (index < 0 || line.indexOf(phrase, index + 1) >= 0) return null;