@runeditrun/rer 0.2.1 → 0.2.4

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 (121) hide show
  1. package/README.md +21 -4
  2. package/cli/README.md +21 -0
  3. package/cli/auth-copy.mjs +25 -18
  4. package/cli/bounded.mjs +13 -4
  5. package/cli/container.mjs +11 -10
  6. package/cli/contract.mjs +2 -2
  7. package/cli/demo-host.mjs +47 -0
  8. package/cli/demo.mjs +47 -0
  9. package/cli/dependencies.mjs +26 -4
  10. package/cli/deploy-command.mjs +94 -0
  11. package/cli/deploy-inputs.mjs +55 -0
  12. package/cli/deploy-observation.mjs +28 -0
  13. package/cli/deploy-owner.mjs +76 -0
  14. package/cli/deploy-readiness.mjs +16 -0
  15. package/cli/deploy.mjs +99 -17
  16. package/cli/detached-server.mjs +1 -1
  17. package/cli/door.mjs +1 -1
  18. package/cli/export.mjs +1 -1
  19. package/cli/hosted-output.mjs +2 -1
  20. package/cli/inspect.mjs +24 -32
  21. package/cli/lifecycle.mjs +31 -8
  22. package/cli/local.mjs +7 -0
  23. package/cli/main.mjs +1 -1
  24. package/cli/managed-server.mjs +27 -18
  25. package/cli/parse.mjs +9 -2
  26. package/cli/pnpm-probe.mjs +17 -0
  27. package/cli/pnpm-runtime.mjs +22 -22
  28. package/cli/portability-policy.mjs +55 -0
  29. package/cli/portability-worker.mjs +8 -4
  30. package/cli/portability.mjs +24 -5
  31. package/cli/project-pnpm.mjs +67 -0
  32. package/cli/release.mjs +35 -0
  33. package/cli/scheduled.mjs +2 -2
  34. package/cli/server-registry.mjs +2 -2
  35. package/cli/setup-input.mjs +1 -1
  36. package/cli/site-check.mjs +15 -6
  37. package/cli/site-data.mjs +6 -5
  38. package/cli/site-references.mjs +153 -3
  39. package/global/CHANGELOG.md +60 -0
  40. package/global/README.md +21 -4
  41. package/global/cli/install.mjs +26 -9
  42. package/global/cli/path-link.mjs +30 -0
  43. package/global/cli/status.mjs +1 -1
  44. package/global/cli/uninstall.mjs +5 -1
  45. package/global/launch/capacity.mjs +1 -1
  46. package/global/launch/facts.mjs +12 -6
  47. package/global/launch/install-pinned.mjs +30 -2
  48. package/global/launch/prepare.mjs +20 -8
  49. package/global/launch/state.mjs +54 -1
  50. package/global/main.mjs +43 -20
  51. package/global/registry.mjs +2 -2
  52. package/global/update/command.mjs +2 -1
  53. package/modules/container/build.mjs +4 -2
  54. package/modules/container/deploy-input.mjs +5 -9
  55. package/modules/container/host.mjs +1 -1
  56. package/modules/container/ingress.mjs +4 -1
  57. package/modules/container/migrations.mjs +2 -1
  58. package/modules/container/worker-engine.mjs +3 -1
  59. package/modules/demo-host/README.md +30 -0
  60. package/modules/demo-host/capacity.mjs +19 -0
  61. package/modules/demo-host/claim.mjs +21 -0
  62. package/modules/demo-host/container.mjs +74 -0
  63. package/modules/demo-host/pages.mjs +56 -0
  64. package/modules/demo-host/plan.mjs +52 -0
  65. package/modules/demo-host/worker.mjs +72 -0
  66. package/modules/dependencies/auth.mjs +2 -1
  67. package/modules/deploy-kit/README.md +52 -7
  68. package/modules/deploy-kit/d1-import.mjs +35 -0
  69. package/modules/deploy-kit/deploy.mjs +4 -3
  70. package/modules/deploy-kit/diagnostics.mjs +11 -1
  71. package/modules/deploy-kit/env-file.mjs +27 -0
  72. package/modules/deploy-kit/health-request.mjs +13 -3
  73. package/modules/deploy-kit/health.mjs +35 -15
  74. package/modules/deploy-kit/migration-ledger.mjs +85 -0
  75. package/modules/deploy-kit/migrations.mjs +48 -16
  76. package/modules/deploy-kit/owner-cli.mjs +42 -8
  77. package/modules/deploy-kit/owner-remove.mjs +108 -0
  78. package/modules/deploy-kit/owner-secret.mjs +94 -0
  79. package/modules/deploy-kit/owner.mjs +124 -19
  80. package/modules/deploy-kit/prepare.mjs +3 -1
  81. package/modules/deploy-kit/provider.mjs +18 -2
  82. package/modules/erasure/README.md +2 -3
  83. package/modules/job-queue/config.mjs +4 -2
  84. package/modules/operator-auth/accounts/settings-page.mjs +8 -3
  85. package/modules/operator-auth/csp.mjs +26 -0
  86. package/modules/operator-auth/dev/badge.mjs +1 -22
  87. package/modules/operator-auth/identity/oidc/README.md +18 -0
  88. package/modules/operator-auth/identity/oidc/config.mjs +4 -2
  89. package/modules/operator-auth/identity/oidc/index.d.mts +1 -1
  90. package/modules/operator-auth/identity/oidc/index.mjs +28 -9
  91. package/modules/operator-auth/identity/oidc/network.mjs +9 -4
  92. package/modules/operator-auth/identity/oidc/store.mjs +19 -3
  93. package/modules/operator-auth/index.d.mts +2 -0
  94. package/modules/operator-auth/page.mjs +2 -2
  95. package/modules/operator-auth/routes.mjs +2 -2
  96. package/modules/portability/README.md +37 -0
  97. package/modules/portability/bridge.mjs +14 -4
  98. package/modules/portability/catalogue.mjs +9 -2
  99. package/modules/portability/import.mjs +4 -2
  100. package/modules/portability/index.d.mts +4 -0
  101. package/modules/portability/index.mjs +1 -0
  102. package/modules/portability/lines.mjs +3 -1
  103. package/modules/release/produce.mjs +1 -1
  104. package/modules/release/source.mjs +5 -2
  105. package/onboarding/adapter-labels.mjs +1 -0
  106. package/onboarding/adapter.mjs +9 -4
  107. package/onboarding/commands.mjs +3 -1
  108. package/onboarding/dependency-environment.mjs +33 -0
  109. package/onboarding/local/owner.mjs +1 -1
  110. package/onboarding/operator-installer.mjs +10 -4
  111. package/onboarding/redaction.mjs +22 -3
  112. package/package.json +6 -1
  113. package/scripts/publish-check/README.md +22 -1
  114. package/scripts/publish-check/c2pa.py +121 -0
  115. package/scripts/publish-check/content_identities.py +3 -1
  116. package/scripts/publish-check/controller_metadata.py +31 -0
  117. package/scripts/publish-check/history_scan.py +13 -3
  118. package/scripts/publish-check/media.py +283 -0
  119. package/scripts/publish-check/png_text.py +34 -0
  120. package/scripts/publish-check/public_hygiene.py +39 -4
  121. package/scripts/publish-check/publication-config.json +7 -1
@@ -0,0 +1,283 @@
1
+ """Find the pixel and sample data inside an image or video file.
2
+
3
+ A file is media only when its bytes walk as one complete container, from the
4
+ signature to the end of the file, allowing only NUL padding after it. The path
5
+ and extension never decide, so text renamed .gif, or a valid image with text
6
+ appended, is not media. Each walker returns where its container ends and the
7
+ (start, end) spans of its pixel or sample data (compressed samples and colour
8
+ tables), or None. Everything outside those spans, such as comments, XMP and
9
+ tags, is still text to inspect.
10
+ """
11
+
12
+
13
+ def gif(data):
14
+ if data[:6] not in (b'GIF87a', b'GIF89a') or len(data) < 13:
15
+ return None
16
+ # Colour tables are pixel data too: palette bytes can spell an address.
17
+ offset = 13 + ((3 << ((data[10] & 7) + 1)) if data[10] & 0x80 else 0)
18
+ samples = [(13, offset)]
19
+ while offset < len(data):
20
+ kind = data[offset]
21
+ if kind == 0x3B:
22
+ return offset + 1, samples
23
+ if kind == 0x21:
24
+ offset += 2
25
+ elif kind == 0x2C:
26
+ if offset + 10 > len(data):
27
+ return None
28
+ flags = data[offset + 9]
29
+ # Descriptor, local colour table, then the LZW minimum code size.
30
+ table = offset + 10 + ((3 << ((flags & 7) + 1)) if flags & 0x80 else 0)
31
+ samples.append((offset + 10, table))
32
+ offset = table + 1
33
+ else:
34
+ return None
35
+ # Data sub-blocks: a length byte, that many bytes, ending at length zero.
36
+ start = offset
37
+ while True:
38
+ if offset >= len(data):
39
+ return None
40
+ size = data[offset]
41
+ offset += 1 + size
42
+ if not size:
43
+ break
44
+ if kind == 0x2C:
45
+ samples.append((start, offset))
46
+ return None
47
+
48
+
49
+ def png(data):
50
+ if not data.startswith(b'\x89PNG\r\n\x1a\n'):
51
+ return None
52
+ offset = 8
53
+ samples = []
54
+ while offset + 12 <= len(data):
55
+ length = int.from_bytes(data[offset:offset + 4], 'big')
56
+ kind = data[offset + 4:offset + 8]
57
+ if kind in (b'PLTE', b'IDAT', b'fdAT'):
58
+ samples.append((offset + 8, offset + 8 + length))
59
+ offset += 12 + length
60
+ if kind == b'IEND':
61
+ return offset, samples
62
+ return None
63
+
64
+
65
+ def jpeg(data):
66
+ # A phone stores more images after the first one's end (MPF depth and
67
+ # gain maps): each is walked, and their samples are pixels too.
68
+ walked = jpeg_image(data, 0)
69
+ if walked is None:
70
+ return None
71
+ end, samples = walked
72
+ while data.startswith(b'\xff\xd8', end) and (following := jpeg_image(data, end)):
73
+ end, more = following
74
+ samples += more
75
+ return end, samples
76
+
77
+
78
+ def jpeg_image(data, offset):
79
+ if not data.startswith(b'\xff\xd8', offset):
80
+ return None
81
+ offset += 2
82
+ samples = []
83
+ while offset + 2 <= len(data):
84
+ if data[offset] != 0xFF:
85
+ return None
86
+ marker = data[offset + 1]
87
+ if marker == 0xFF:
88
+ offset += 1
89
+ continue
90
+ if marker == 0xD9:
91
+ return offset + 2, samples
92
+ if 0xD0 <= marker <= 0xD7 or marker == 0x01:
93
+ offset += 2
94
+ continue
95
+ if offset + 4 > len(data):
96
+ return None
97
+ offset += 2 + int.from_bytes(data[offset + 2:offset + 4], 'big')
98
+ if marker == 0xDA:
99
+ # Entropy-coded data runs to the next marker other than a stuffed
100
+ # zero or a restart marker.
101
+ start = offset
102
+ while True:
103
+ offset = data.find(b'\xff', offset)
104
+ if offset < 0 or offset + 1 >= len(data):
105
+ return None
106
+ if data[offset + 1] == 0 or 0xD0 <= data[offset + 1] <= 0xD7:
107
+ offset += 2
108
+ else:
109
+ break
110
+ samples.append((start, offset))
111
+ return None
112
+
113
+
114
+ WEBP_SAMPLES = (b'VP8 ', b'VP8L', b'ALPH', b'ANMF')
115
+
116
+
117
+ def webp(data):
118
+ if len(data) < 20 or data[:4] != b'RIFF' or data[8:12] != b'WEBP':
119
+ return None
120
+ end = int.from_bytes(data[4:8], 'little') + 8
121
+ offset = 12
122
+ samples = []
123
+ while offset + 8 <= end:
124
+ size = int.from_bytes(data[offset + 4:offset + 8], 'little')
125
+ if data[offset:offset + 4] in WEBP_SAMPLES:
126
+ samples.append((offset + 8, offset + 8 + size))
127
+ offset += 8 + size + (size & 1)
128
+ return (end, samples) if offset == end else None
129
+
130
+
131
+ # Major brands of MP4, QuickTime, 3GPP and HEIF/AVIF files.
132
+ ISO_BRANDS = {b'isom', b'iso2', b'iso4', b'iso5', b'iso6', b'mp41', b'mp42', b'avc1', b'dash',
133
+ b'M4V ', b'M4A ', b'M4VH', b'M4VP', b'qt ', b'3gp4', b'3gp5', b'3gp6', b'3g2a',
134
+ b'mif1', b'msf1', b'avif', b'avis', b'heic', b'heix', b'heim', b'heis', b'hevc', b'hevx'}
135
+
136
+
137
+ def iso_media(data):
138
+ # MP4 and AVIF share ISO base media boxes, opened by a file-type box with
139
+ # an explicit size and a known major brand.
140
+ if len(data) < 16 or data[4:8] != b'ftyp' or data[8:12] not in ISO_BRANDS:
141
+ return None
142
+ if int.from_bytes(data[:4], 'big') < 16:
143
+ return None
144
+ offset = 0
145
+ kinds, samples = set(), []
146
+ while offset + 8 <= len(data):
147
+ size = int.from_bytes(data[offset:offset + 4], 'big')
148
+ kind = data[offset + 4:offset + 8]
149
+ header = 8
150
+ if size == 1:
151
+ if offset + 16 > len(data):
152
+ return None
153
+ size, header = int.from_bytes(data[offset + 8:offset + 16], 'big'), 16
154
+ elif size == 0:
155
+ # Only a last box after the file type may run to the end.
156
+ size = len(data) - offset
157
+ if size < header:
158
+ return None
159
+ kinds.add(kind)
160
+ if kind == b'mdat':
161
+ samples.append((offset + header, offset + size))
162
+ offset += size
163
+ # A movie has its moov box; a still image (HEIF/AVIF) has meta.
164
+ if offset != len(data) or not kinds & {b'moov', b'meta'}:
165
+ return None
166
+ return offset, samples
167
+
168
+
169
+ def ebml_number(data, offset, marker):
170
+ # A variable-length EBML number; the marker bit stays in IDs, not sizes.
171
+ if offset >= len(data) or not data[offset]:
172
+ return None, offset
173
+ length = 9 - data[offset].bit_length()
174
+ if offset + length > len(data):
175
+ return None, offset
176
+ value = int.from_bytes(data[offset:offset + length], 'big')
177
+ if not marker:
178
+ value &= (1 << (7 * length)) - 1
179
+ if value == (1 << (7 * length)) - 1:
180
+ value = -1
181
+ return value, offset + length
182
+
183
+
184
+ SEGMENT = 0x18538067
185
+ CLUSTER = 0x1F43B675
186
+ # Segment children: a Cluster of unknown size ends at the next one of these.
187
+ SEGMENT_CHILDREN = {CLUSTER, 0x114D9B74, 0x1549A966, 0x1654AE6B, 0x1C53BB6B,
188
+ 0x1941A469, 0x1043A770, 0x1254C367}
189
+
190
+
191
+ def ebml_element(data, offset):
192
+ element, content = ebml_number(data, offset, True)
193
+ size, content = ebml_number(data, content, False)
194
+ return element, content, size
195
+
196
+
197
+ def open_cluster(data, offset, end):
198
+ # A live Cluster of unknown size runs to the next Segment child.
199
+ start = offset
200
+ while offset < end:
201
+ element, content, size = ebml_element(data, offset)
202
+ if element in SEGMENT_CHILDREN:
203
+ break
204
+ if element is None or size is None or size == -1:
205
+ return None
206
+ offset = content + size
207
+ return offset - start
208
+
209
+
210
+ def webm_segment(data, offset, end):
211
+ # Each Cluster's blocks are the samples; other Segment children are not.
212
+ samples = []
213
+ while offset < end:
214
+ element, content, size = ebml_element(data, offset)
215
+ if element is None or size is None:
216
+ return None
217
+ if size == -1:
218
+ size = open_cluster(data, content, end) if element == CLUSTER else None
219
+ if size is None:
220
+ return None
221
+ if element == CLUSTER:
222
+ samples.append((content, content + size))
223
+ offset = content + size
224
+ return samples if offset == end else None
225
+
226
+
227
+ def webm(data):
228
+ if not data.startswith(b'\x1a\x45\xdf\xa3') or b'webm' not in data[:64]:
229
+ return None
230
+ offset, samples = 0, []
231
+ while offset < len(data):
232
+ element, content, size = ebml_element(data, offset)
233
+ if element is None or size is None:
234
+ return None
235
+ # Only a live Segment may leave its size unknown: it runs to the end.
236
+ if size == -1:
237
+ if element != SEGMENT:
238
+ return None
239
+ size = len(data) - content
240
+ if element == SEGMENT:
241
+ segment = webm_segment(data, content, content + size)
242
+ if segment is None:
243
+ return None
244
+ samples += segment
245
+ offset = content + size
246
+ return (offset, samples) if offset == len(data) else None
247
+
248
+
249
+ def ico(data):
250
+ if len(data) < 6 or data[:4] != b'\0\0\1\0':
251
+ return None
252
+ count = int.from_bytes(data[4:6], 'little')
253
+ images = []
254
+ for index in range(count):
255
+ entry = 6 + 16 * index
256
+ if entry + 16 > len(data):
257
+ return None
258
+ images.append((int.from_bytes(data[entry + 12:entry + 16], 'little'),
259
+ int.from_bytes(data[entry + 8:entry + 12], 'little')))
260
+ # Image data follows the directory with no gap. An embedded PNG keeps its
261
+ # own compressed samples; bitmap pixels are stored as they are.
262
+ offset = 6 + 16 * count
263
+ samples = []
264
+ for start, size in sorted(images):
265
+ if start != offset or not size:
266
+ return None
267
+ image = png(data[start:start + size])
268
+ if image and image[0] == size:
269
+ samples += [(start + a, start + b) for a, b in image[1]]
270
+ offset += size
271
+ return (offset, samples) if count else None
272
+
273
+
274
+ FORMATS = (gif, png, jpeg, webp, iso_media, webm, ico)
275
+
276
+
277
+ def sample_spans(data):
278
+ """The pixel and sample spans of a complete media file, or None."""
279
+ for walk in FORMATS:
280
+ walked = walk(data)
281
+ if walked and walked[0] <= len(data) and not data[walked[0]:].strip(b'\0'):
282
+ return [(start, min(end, walked[0])) for start, end in walked[1]]
283
+ return None
@@ -0,0 +1,34 @@
1
+ """Separate uncompressed PNG text fields from chunk framing and CRC bytes."""
2
+
3
+
4
+ def text_chunks(data):
5
+ # Detect the container by bytes, never by a publication path or extension.
6
+ if not data.startswith(b'\x89PNG\r\n\x1a\n'):
7
+ return
8
+ offset = 8
9
+ while offset + 12 <= len(data):
10
+ start = offset
11
+ length = int.from_bytes(data[offset:offset + 4], 'big')
12
+ end = offset + 8 + length
13
+ if end + 4 > len(data):
14
+ return
15
+ kind = data[offset + 4:offset + 8]
16
+ payload = data[offset + 8:end]
17
+ offset = end + 4
18
+ # Checksums do not confer an exemption: inspect a framed text field
19
+ # even if its CRC is wrong. Non-text chunks keep raw-byte inspection.
20
+ if kind == b'tEXt':
21
+ fields = payload.split(b'\0', 1)
22
+ if len(fields) == 2:
23
+ yield start, offset, [field.decode('latin-1').encode('utf-8') for field in fields]
24
+ elif kind == b'iTXt':
25
+ fields = payload.split(b'\0', 1)
26
+ if len(fields) != 2 or len(fields[1]) < 2:
27
+ continue
28
+ keyword, body = fields
29
+ fields = body[2:].split(b'\0', 2)
30
+ # The compression method is ignored when the flag is zero.
31
+ if body[0] == 0 and len(fields) == 3:
32
+ yield start, offset, [keyword.decode('latin-1').encode('utf-8'), *fields]
33
+ if kind == b'IEND':
34
+ return
@@ -8,6 +8,9 @@ from provenance import provenance_text
8
8
  from hygiene_config import EMAIL, LOCAL_PATH, public_email
9
9
  from font_licence import without_required_notices
10
10
  from trust_bundle import anchor_values
11
+ from png_text import text_chunks
12
+ from media import sample_spans
13
+ from c2pa import certificate_spans
11
14
  URI_AUTHORITY = re.compile(r'''(?i)(?a:(?P<scheme>[a-z][a-z0-9+.-]*:))?//(?P<authority>[^/?#\s<>"']+)''')
12
15
  # Match raw, complete commit trailer lines; decoded or embedded prose cannot
13
16
  # acquire trailer privileges. Only the address span is omitted from email checks.
@@ -47,7 +50,7 @@ def email_userinfo(match):
47
50
  def private_emails(text, config):
48
51
  # URL paths, queries and fragments remain visible to email inspection.
49
52
  email_text = URI_AUTHORITY.sub(email_userinfo, without_required_notices(text))
50
- email_text = re.sub(r'(?i)\b(?a:git@github\.com:)(?=[A-Za-z0-9])', '', email_text)
53
+ email_text = re.sub(r'(?i)\b(?a:git@github\.com:)(?=[A-Za-z0-9]|\$\{[A-Za-z_$][A-Za-z0-9_.$]*\})', '', email_text)
51
54
  # A package manager's patch filename joins the name and version with an at sign.
52
55
  version = r'(?:v?[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9-]+)?|[a-f0-9]{40})(?:\.patch)?(?![A-Za-z0-9_.-])'
53
56
  email_text = re.sub(r'(?m)^\s*-?\s*uses:\s*[\"\']?[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[^\s\"\']+', '', email_text)
@@ -92,11 +95,43 @@ def certificate_metadata(data, bundle, emails, config):
92
95
  return not Counter(emails) - inside
93
96
 
94
97
 
98
+ def without_spans(data, spans):
99
+ # Each span becomes one space: text on either side stays apart, and no
100
+ # long blank run is left for the line-anchored patterns to rescan.
101
+ kept, offset = [], 0
102
+ for start, end in sorted(spans):
103
+ kept.append(data[offset:start])
104
+ offset = max(offset, end)
105
+ kept.append(data[offset:])
106
+ return b' '.join(kept)
107
+
108
+
95
109
  def inspect(data, path, config, resolver, *, commit_message=False):
96
- return inspect_text(data, path, config, resolver, commit_message=commit_message)
110
+ chunks = list(text_chunks(data))
111
+ # Image and video samples are not text: they can spell an address or a
112
+ # path by chance. Comments, XMP, tags and other metadata remain text.
113
+ spans = sample_spans(data) or []
114
+ # Names in a C2PA manifest's signing certificates are certificate metadata.
115
+ spans += certificate_spans(data)
116
+ rules, provenance = set(), False
117
+ for start, end, fields in chunks:
118
+ spans.append((start, end))
119
+ for field in fields:
120
+ # A metadata value is text, not a standalone source manifest or
121
+ # trust bundle, even when the containing image has a JSON name.
122
+ field_rules, field_provenance = inspect_text(field, '@png-text', config, resolver, binary_content=False)
123
+ rules.update(field_rules)
124
+ provenance |= field_provenance
125
+ # Keep the original binary classification for content outside text chunks.
126
+ # CRC-adjacent bytes must not also create a synthetic raw mailbox match.
127
+ raw_rules, raw_provenance = inspect_text(without_spans(data, spans), path, config, resolver,
128
+ commit_message=commit_message, binary_content=binary(data))
129
+ rules.update(raw_rules)
130
+ provenance |= raw_provenance
131
+ return rules, provenance
97
132
 
98
133
 
99
- def inspect_text(data, path, config, resolver, *, commit_message=False):
134
+ def inspect_text(data, path, config, resolver, *, commit_message=False, binary_content=None):
100
135
  # NUL removal covers UTF-16/32 ASCII content, matching the secret scanner.
101
136
  text = decoded_text(data)
102
137
  rules = set()
@@ -111,7 +146,7 @@ def inspect_text(data, path, config, resolver, *, commit_message=False):
111
146
  if emails and not (bundle and certificate_metadata(data, bundle, emails, config)):
112
147
  # Inside binary data a mailbox is judged by its own shape: a host
113
148
  # name with mixed-case letters inside a label is not one.
114
- if not binary(data) or any(stored_host(email) for email in emails):
149
+ if not (binary(data) if binary_content is None else binary_content) or any(stored_host(email) for email in emails):
115
150
  rules.add('hygiene.email')
116
151
  ref_text, provenance = provenance_text(text, path, config.get('declaredSources', frozenset()))
117
152
  stated, ambiguous = reference_sets(ref_text, path, config.get('localPaths', frozenset()), config.get('projectIdentity'))
@@ -22,6 +22,12 @@
22
22
  { "repository": "just-every/run-oncall", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
23
23
  { "repository": "just-every/run-spool", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
24
24
  { "repository": "just-every/run-happytohelp", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
25
- { "repository": "just-every/run-auth", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." }
25
+ { "repository": "just-every/run-auth", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
26
+ { "repository": "just-every/run-chest", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
27
+ { "repository": "just-every/run-purse", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
28
+ { "repository": "just-every/run-easel", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
29
+ { "repository": "just-every/run-willow", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
30
+ { "repository": "just-every/run-pier", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
31
+ { "repository": "just-every/run-linen", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." }
26
32
  ]
27
33
  }