igniter_lang 0.1.0.alpha.1

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.
@@ -0,0 +1,802 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OOF/Fragment Registry internal validator.
4
+ #
5
+ # ISOLATION CONTRACT — this file must not:
6
+ # - be required from lib/igniter_lang.rb
7
+ # - integrate with any compiler pass (parser/classifier/TypeChecker/SemanticIR/assembler)
8
+ # - emit public diagnostics
9
+ # - write to report["diagnostics"] or top-level compilation reports
10
+ # - add CompilerResult fields
11
+ # - expose public API or CLI
12
+ # - call runtime, Ledger/TBackend, Gate 3, cache, signing, or production behavior
13
+ #
14
+ # Authorized by: LANG-R102-A (registry validator)
15
+ # LANG-R110-A (source-envelope helper)
16
+ # LANG-R121-A (profile/pack source acceptance helper slice)
17
+ # Tracks: oof-fragment-registry-implementation-boundary-proof-v0
18
+ # oof-fragment-registry-source-envelope-helper-proof-v0
19
+ #
20
+ # R92 historical note: the shadow proof JSON at
21
+ # experiments/oof_fragment_registry_shadow_proof/out/oof_descriptors.shadow_registry.json
22
+ # placed PINV-*/TINV-* inside oof_descriptors as historical proof evidence.
23
+ # That placement is NON-FORWARD. The forward shape (authorized by LANG-R98-A
24
+ # and LANG-R101-D1) places PINV-*/TINV-* exclusively under
25
+ # support_markers.invariant_support_markers. This validator enforces the
26
+ # forward shape.
27
+
28
+ require "set"
29
+
30
+ module IgniterLang
31
+ class OOFFragmentRegistry
32
+ FORMAT_VERSION = "0.1.0".freeze
33
+
34
+ # Namespace prefixes that must be present in excluded_namespaces.
35
+ REQUIRED_EXCLUDED_PREFIXES = %w[
36
+ compiler_profile_contract.
37
+ compiler_profile_contract_refusal.
38
+ ].freeze
39
+
40
+ # Support marker code pattern: only PINV-* and TINV-* are support markers.
41
+ SUPPORT_MARKER_PATTERN = /\A(PINV|TINV)-/.freeze
42
+
43
+ # Fragment row names that are guarded non-fragments (must have classification_kind "not_fragment_class").
44
+ GUARDED_NON_FRAGMENT_NAMES = %w[olap progression].freeze
45
+
46
+ # The special "oof" fragment row that must be non-loadable and capability-free.
47
+ OOF_ROW_NAME = "oof".freeze
48
+
49
+ # Acceptable public_code_stability values for support markers (non-public).
50
+ SUPPORT_MARKER_STABILITY_VALUES = %w[non_public_support_marker proof_only].freeze
51
+
52
+ # -------------------------------------------------------------------------
53
+ # Source-envelope helper — accepted and held/rejected source modes.
54
+ # Authorized by: LANG-R110-A
55
+ # These constants are internal to this helper and are NOT public API.
56
+ # -------------------------------------------------------------------------
57
+
58
+ # Accepted source modes for validate_source_envelope.
59
+ SOURCE_ACCEPTED_MODES = %w[
60
+ proof_fixture
61
+ caller_supplied
62
+ profile_candidate
63
+ pack_descriptor_candidate
64
+ ].freeze
65
+
66
+ # Held modes: recognized but not yet authorized for helper processing.
67
+ SOURCE_HELD_MODES = [].freeze
68
+
69
+ # Accepted authority kinds for source envelope.
70
+ SOURCE_ACCEPTED_AUTHORITY_KINDS = %w[proof_only design_accepted].freeze
71
+
72
+ # Accepted (non-canon) canon_status values.
73
+ SOURCE_ACCEPTED_CANON_STATUSES = %w[non_canon accepted_design].freeze
74
+
75
+ # Source-envelope helper diagnostic codes.
76
+ # These are internal helper diagnostics. They are NOT language OOF codes and
77
+ # are NOT central IgniterLang::Diagnostics entries.
78
+ SOURCE_DIAG_WRONG_KIND = "oof_registry.source.validation.wrong_kind".freeze
79
+ SOURCE_DIAG_UNSUPPORTED_FORMAT_VERSION = "oof_registry.source.validation.unsupported_format_version".freeze
80
+ SOURCE_DIAG_UNSUPPORTED_SOURCE_MODE = "oof_registry.source.validation.unsupported_source_mode".freeze
81
+ SOURCE_DIAG_HELD_SOURCE_MODE = "oof_registry.source.validation.held_source_mode".freeze
82
+ SOURCE_DIAG_INVALID_AUTHORITY_KIND = "oof_registry.source.validation.invalid_authority_kind".freeze
83
+ SOURCE_DIAG_CANON_STATUS_FORBIDDEN = "oof_registry.source.validation.canon_status_forbidden".freeze
84
+ SOURCE_DIAG_MISSING_AUTHORITY = "oof_registry.source.validation.missing_authority".freeze
85
+ SOURCE_DIAG_MISSING_AUTHORITY_REF = "oof_registry.source.validation.missing_authority_ref".freeze
86
+ SOURCE_DIAG_MISSING_REGISTRY = "oof_registry.source.validation.missing_registry".freeze
87
+ SOURCE_DIAG_SURFACE_OPEN = "oof_registry.source.validation.surface_open".freeze
88
+ SOURCE_DIAG_MISSING_PACK_REF = "oof_registry.source.validation.missing_pack_ref".freeze
89
+ SOURCE_DIAG_MISSING_PROFILE_REF = "oof_registry.source.validation.missing_profile_ref".freeze
90
+ SOURCE_DIAG_MISSING_SELECTED_PACK_REF = "oof_registry.source.validation.missing_selected_pack_ref".freeze
91
+ SOURCE_DIAG_MISSING_PACK_DESCRIPTOR = "oof_registry.source.validation.missing_pack_descriptor".freeze
92
+ SOURCE_DIAG_ROW_OWNER_MISMATCH = "oof_registry.source.validation.row_owner_mismatch".freeze
93
+ SOURCE_DIAG_DUPLICATE_ROW_OWNERSHIP = "oof_registry.source.validation.duplicate_row_ownership".freeze
94
+ SOURCE_DIAG_DUPLICATE_ALIAS_OWNERSHIP = "oof_registry.source.validation.duplicate_alias_ownership".freeze
95
+ SOURCE_DIAG_EXCLUDED_NAMESPACE_CLAIM = "oof_registry.source.validation.excluded_namespace_claim".freeze
96
+ SOURCE_DIAG_PROFILE_OVERRIDE_FORBIDDEN = "oof_registry.source.validation.profile_override_forbidden".freeze
97
+ SOURCE_DIAG_INVALID_CONFLICT_POLICY = "oof_registry.source.validation.invalid_conflict_policy".freeze
98
+ SOURCE_DIAG_INVALID_PACK_ORDER = "oof_registry.source.validation.invalid_pack_order".freeze
99
+
100
+ # -------------------------------------------------------------------------
101
+ # Internal validator diagnostic codes.
102
+ # These are NOT public language OOF codes and are NOT central IgniterLang::Diagnostics entries.
103
+ DIAG_MISSING_SECTION = "oof_registry.validation.missing_section".freeze
104
+ DIAG_WRONG_KIND = "oof_registry.validation.wrong_kind".freeze
105
+ DIAG_DUPLICATE_CODE = "oof_registry.validation.duplicate_code".freeze
106
+ DIAG_ALIAS_COLLISION = "oof_registry.validation.alias_collision".freeze
107
+ DIAG_ALIAS_MISSING_REPLACEMENT = "oof_registry.validation.alias_missing_replacement".freeze
108
+ DIAG_EXCLUDED_NAMESPACE_COLLISION = "oof_registry.validation.excluded_namespace_collision".freeze
109
+ DIAG_SUPPORT_MARKER_IN_DESCRIPTORS = "oof_registry.validation.support_marker_in_oof_descriptors".freeze
110
+ DIAG_SUPPORT_MARKER_PUBLIC = "oof_registry.validation.support_marker_public".freeze
111
+ DIAG_SUPPORT_MARKER_EMITTED = "oof_registry.validation.support_marker_emitted".freeze
112
+ DIAG_SUPPORT_MARKER_CODE_COLLISION = "oof_registry.validation.support_marker_code_collision".freeze
113
+ DIAG_OOF_PROJECTION_LOADABLE = "oof_registry.validation.oof_projection_loadable".freeze
114
+ DIAG_OOF_PROJECTION_CAPABILITY = "oof_registry.validation.oof_projection_capability".freeze
115
+ DIAG_GUARDED_NON_FRAGMENT = "oof_registry.validation.guarded_non_fragment_violation".freeze
116
+ DIAG_OWNER_BOUNDARY_ABSENT = "oof_registry.validation.owner_boundary_absent".freeze
117
+
118
+ # Validate a registry hash against the R101 forward bucket shape.
119
+ #
120
+ # @param registry [Hash] The registry object to validate (caller-supplied).
121
+ # @param installed_boundaries [Array<String>, nil]
122
+ # When supplied, rows owned by a boundary not in this list are recorded
123
+ # as inactive rows. Inactive rows are NOT silently skipped — they are
124
+ # recorded in the result. Inactive rows do not flip valid: false.
125
+ # When nil, boundary-absence checks are skipped.
126
+ #
127
+ # @return [Hash] Internal validation result.
128
+ # NEVER touches compiler state, reports, or public surfaces.
129
+ def validate(registry, installed_boundaries: nil)
130
+ diags = []
131
+ inactive_rows = []
132
+
133
+ # Step 1 — top-level shape
134
+ shape_diags = check_top_level_shape(registry)
135
+ diags.concat(shape_diags)
136
+ return build_result(false, diags, inactive_rows) unless shape_diags.empty?
137
+
138
+ oof_descriptors = Array(registry["oof_descriptors"])
139
+ fragment_rows = Array(registry["fragment_rows"])
140
+ support_markers_arr = Array(registry.dig("support_markers", "invariant_support_markers"))
141
+ excluded_namespaces = Array(registry["excluded_namespaces"])
142
+ excluded_prefixes = excluded_namespaces.map { |n| n["prefix"] }.compact
143
+
144
+ # Pre-collect all canonical OOF codes + aliases for cross-collision detection
145
+ all_oof_canonical = oof_descriptors.map { |d| d["code"] }.compact.to_set
146
+ all_oof_aliases = oof_descriptors.flat_map { |d| Array(d["aliases"]) }.to_set
147
+ all_oof_code_set = all_oof_canonical | all_oof_aliases
148
+
149
+ # Step 2 — OOF descriptors
150
+ diags.concat(check_oof_descriptors(oof_descriptors, excluded_prefixes, all_oof_canonical))
151
+
152
+ # Step 3 — fragment rows
153
+ diags.concat(check_fragment_rows(fragment_rows))
154
+
155
+ # Step 4 — support markers
156
+ diags.concat(check_support_markers(support_markers_arr, all_oof_code_set))
157
+
158
+ # Step 5 — excluded namespaces
159
+ diags.concat(check_excluded_namespaces(excluded_namespaces))
160
+
161
+ # Step 6 — absent-owner inactive rows (recorded, not silently skipped)
162
+ if installed_boundaries
163
+ boundaries_set = installed_boundaries.to_set
164
+ candidates = [
165
+ *oof_descriptors.map { |row| ["oof_descriptor", row["code"] || row["name"], row] },
166
+ *fragment_rows.map { |row| ["fragment_row", row["name"], row] },
167
+ *support_markers_arr.map { |row| ["support_marker", row["code"] || row["name"], row] }
168
+ ]
169
+ candidates.each do |section_kind, row_id, row|
170
+ owner = row["owner_pack_or_boundary"]
171
+ next unless owner
172
+ next if boundaries_set.include?(owner)
173
+
174
+ inactive_rows << {
175
+ "section" => section_kind,
176
+ "row_id" => row_id,
177
+ "owner" => owner,
178
+ "reason" => "owner_pack_or_boundary_absent_from_installed_boundaries"
179
+ }
180
+ end
181
+ end
182
+
183
+ build_result(diags.empty?, diags, inactive_rows)
184
+ end
185
+
186
+ # Validate a source envelope and, if valid, validate its nested registry.
187
+ #
188
+ # This is an internal helper only. It is NOT a public API, NOT a loader,
189
+ # NOT a compiler pass, and NOT a report surface. It is callable only from
190
+ # proof-local harnesses via direct require of this file.
191
+ #
192
+ # Authorized by: LANG-R110-A
193
+ # Design: oof-fragment-registry-source-envelope-helper-boundary-design-v0 (LANG-R109-D1)
194
+ #
195
+ # @param source_envelope [Hash] A source envelope describing where the registry
196
+ # hash comes from. Must have kind, format_version, source_mode, authority, registry.
197
+ # @param installed_boundaries [Array<String>, nil]
198
+ # Forwarded to the nested registry validate call when source envelope passes.
199
+ #
200
+ # @return [Hash] Internal source-envelope validation result.
201
+ # - valid: true only when source-envelope validation AND nested registry validation pass.
202
+ # - source_mode: the source_mode from the envelope (or nil if envelope is malformed).
203
+ # - registry_present: whether the envelope contained a registry hash.
204
+ # - source_diagnostics: internal source-envelope diagnostics only.
205
+ # - registry_validation: the nested registry validation result, or nil if source invalid.
206
+ # - closed_surface_assertions: all false (machine-assertable).
207
+ # NEVER touches compiler state, reports, or public surfaces.
208
+ def validate_source_envelope(source_envelope, installed_boundaries: nil)
209
+ source_diags = []
210
+
211
+ # Step 1 — envelope must be a Hash with correct kind
212
+ unless source_envelope.is_a?(Hash)
213
+ source_diags << source_diag(SOURCE_DIAG_WRONG_KIND,
214
+ "source envelope must be a Hash, got #{source_envelope.class}")
215
+ return build_source_result(false, nil, false, source_diags, nil)
216
+ end
217
+
218
+ if source_envelope["kind"] != "oof_fragment_registry_source"
219
+ source_diags << source_diag(SOURCE_DIAG_WRONG_KIND,
220
+ "source envelope kind must be 'oof_fragment_registry_source', " \
221
+ "got #{source_envelope["kind"].inspect}")
222
+ end
223
+
224
+ # Step 2 — format version
225
+ unless source_envelope["format_version"] == "0.1.0"
226
+ source_diags << source_diag(SOURCE_DIAG_UNSUPPORTED_FORMAT_VERSION,
227
+ "source envelope format_version must be '0.1.0', " \
228
+ "got #{source_envelope["format_version"].inspect}")
229
+ end
230
+
231
+ # Step 3 — source mode
232
+ source_mode = source_envelope["source_mode"]
233
+ if SOURCE_HELD_MODES.include?(source_mode)
234
+ source_diags << source_diag(SOURCE_DIAG_HELD_SOURCE_MODE,
235
+ "source_mode #{source_mode.inspect} is known but held; " \
236
+ "only 'proof_fixture' and 'caller_supplied' are accepted in this helper")
237
+ elsif !SOURCE_ACCEPTED_MODES.include?(source_mode)
238
+ source_diags << source_diag(SOURCE_DIAG_UNSUPPORTED_SOURCE_MODE,
239
+ "source_mode #{source_mode.inspect} is not supported; " \
240
+ "accepted: proof_fixture, caller_supplied")
241
+ end
242
+
243
+ # Step 4 — authority object
244
+ authority = source_envelope["authority"]
245
+ if authority.is_a?(Hash)
246
+ # authority_ref must be present
247
+ if authority["authority_ref"].to_s.strip.empty?
248
+ source_diags << source_diag(SOURCE_DIAG_MISSING_AUTHORITY_REF,
249
+ "authority.authority_ref is required and must be non-empty")
250
+ end
251
+
252
+ # authority_kind must be within proof/design scope
253
+ authority_kind = authority["authority_kind"]
254
+ unless SOURCE_ACCEPTED_AUTHORITY_KINDS.include?(authority_kind)
255
+ source_diags << source_diag(SOURCE_DIAG_INVALID_AUTHORITY_KIND,
256
+ "authority.authority_kind #{authority_kind.inspect} is outside proof/design scope; " \
257
+ "accepted: proof_only, design_accepted")
258
+ end
259
+
260
+ # canon_status must not be canon
261
+ canon_status = authority["canon_status"]
262
+ if canon_status == "canon"
263
+ source_diags << source_diag(SOURCE_DIAG_CANON_STATUS_FORBIDDEN,
264
+ "canon-status source envelopes are forbidden in this helper; " \
265
+ "authority.canon_status must not be 'canon'")
266
+ elsif !SOURCE_ACCEPTED_CANON_STATUSES.include?(canon_status)
267
+ source_diags << source_diag(SOURCE_DIAG_CANON_STATUS_FORBIDDEN,
268
+ "authority.canon_status #{canon_status.inspect} is not an accepted non-canon status; " \
269
+ "accepted: non_canon, accepted_design")
270
+ end
271
+ else
272
+ source_diags << source_diag(SOURCE_DIAG_MISSING_AUTHORITY,
273
+ "source envelope authority object is missing or not a Hash")
274
+ end
275
+
276
+ # Step 5 — nested registry must be present for direct registry sources.
277
+ registry_present = source_envelope["registry"].is_a?(Hash)
278
+ direct_registry_source = %w[proof_fixture caller_supplied].include?(source_mode)
279
+ if direct_registry_source && !registry_present
280
+ source_diags << source_diag(SOURCE_DIAG_MISSING_REGISTRY,
281
+ "source envelope must contain a 'registry' Hash; nested registry is missing or invalid")
282
+ end
283
+
284
+ # Step 6 — closed-surface assertions must all be false
285
+ envelope_assertions = source_envelope.fetch("closed_surface_assertions", nil)
286
+ if envelope_assertions.is_a?(Hash) && !envelope_assertions.values.all?(false)
287
+ open_keys = envelope_assertions.select { |_k, v| v }.keys
288
+ source_diags << source_diag(SOURCE_DIAG_SURFACE_OPEN,
289
+ "source envelope closed_surface_assertions must all be false; " \
290
+ "open assertions: #{open_keys.inspect}")
291
+ end
292
+
293
+ # If source envelope has any diagnostics, do NOT call nested registry validator.
294
+ if source_diags.any?
295
+ return build_source_result(false, source_mode, registry_present, source_diags, nil,
296
+ source_authority_summary(source_envelope))
297
+ end
298
+
299
+ case source_mode
300
+ when "profile_candidate"
301
+ return validate_profile_candidate_source(source_envelope, installed_boundaries: installed_boundaries)
302
+ when "pack_descriptor_candidate"
303
+ return validate_pack_descriptor_candidate_source(source_envelope,
304
+ installed_boundaries: installed_boundaries)
305
+ end
306
+
307
+ # Source envelope passed — call existing nested registry validator.
308
+ registry_result = validate(source_envelope["registry"], installed_boundaries: installed_boundaries)
309
+ source_valid = registry_result.fetch("valid")
310
+
311
+ build_source_result(source_valid, source_mode, true, [], registry_result,
312
+ source_authority_summary(source_envelope))
313
+ end
314
+
315
+ private
316
+
317
+ def validate_pack_descriptor_candidate_source(source_envelope, installed_boundaries:)
318
+ source_diags = pack_descriptor_candidate_diagnostics(source_envelope)
319
+ registry_present = source_envelope["registry"].is_a?(Hash)
320
+
321
+ if source_diags.any?
322
+ return build_source_result(false, "pack_descriptor_candidate", registry_present, source_diags, nil,
323
+ source_authority_summary(source_envelope))
324
+ end
325
+
326
+ registry_validation = registry_present ? validate(source_envelope["registry"], installed_boundaries: installed_boundaries) : nil
327
+ valid = registry_validation.nil? || registry_validation.fetch("valid")
328
+
329
+ build_source_result(valid, "pack_descriptor_candidate", registry_present, [], registry_validation,
330
+ source_authority_summary(source_envelope))
331
+ end
332
+
333
+ def validate_profile_candidate_source(source_envelope, installed_boundaries:)
334
+ source_diags = profile_candidate_schema_diagnostics(source_envelope)
335
+ pack_candidates = Array(source_envelope["pack_descriptor_candidates"])
336
+ pack_candidates.each { |pack| source_diags.concat(pack_descriptor_candidate_diagnostics(pack)) }
337
+ source_diags.concat(profile_authority_diagnostics(source_envelope, pack_candidates))
338
+ source_diags.concat(row_conflict_diagnostics(pack_candidates, source_envelope))
339
+ source_diags.concat(excluded_namespace_claim_diagnostics(pack_candidates))
340
+
341
+ if source_diags.any?
342
+ return build_source_result(false, "profile_candidate", false, source_diags, nil,
343
+ source_authority_summary(source_envelope))
344
+ end
345
+
346
+ derived_registry = derive_registry_from_profile(source_envelope, pack_candidates)
347
+ registry_result = validate(derived_registry, installed_boundaries: installed_boundaries)
348
+ build_source_result(registry_result.fetch("valid"), "profile_candidate", true, [],
349
+ registry_result, source_authority_summary(source_envelope))
350
+ end
351
+
352
+ def pack_descriptor_candidate_diagnostics(pack)
353
+ diags = []
354
+ unless pack.is_a?(Hash)
355
+ return [source_diag(SOURCE_DIAG_MISSING_PACK_DESCRIPTOR,
356
+ "pack_descriptor_candidate entries must be Hash objects")]
357
+ end
358
+
359
+ pack_ref = pack["pack_ref"].to_s
360
+ owner = pack["owner_pack_or_boundary"].to_s
361
+ if pack_ref.strip.empty?
362
+ diags << source_diag(SOURCE_DIAG_MISSING_PACK_REF,
363
+ "pack_descriptor_candidate must include non-empty pack_ref")
364
+ end
365
+ if owner.strip.empty?
366
+ diags << source_diag(DIAG_OWNER_BOUNDARY_ABSENT,
367
+ "pack_descriptor_candidate #{pack_ref.inspect} must include owner_pack_or_boundary")
368
+ end
369
+ unless pack["row_authority_policy"] == "pack_owns_declared_rows"
370
+ diags << source_diag(SOURCE_DIAG_INVALID_CONFLICT_POLICY,
371
+ "pack_descriptor_candidate #{pack_ref.inspect} must set row_authority_policy to " \
372
+ "'pack_owns_declared_rows'")
373
+ end
374
+
375
+ pack_rows(pack).each do |section, row_id, row|
376
+ next if owner.empty?
377
+ next if row["owner_pack_or_boundary"] == owner
378
+
379
+ diags << source_diag(SOURCE_DIAG_ROW_OWNER_MISMATCH,
380
+ "#{section} #{row_id.inspect} owner #{row["owner_pack_or_boundary"].inspect} " \
381
+ "does not match pack owner #{owner.inspect}")
382
+ end
383
+
384
+ diags.concat(excluded_namespace_claim_diagnostics([pack]))
385
+ diags.concat(row_conflict_diagnostics([pack], nil))
386
+ diags
387
+ end
388
+
389
+ def profile_candidate_schema_diagnostics(profile)
390
+ diags = []
391
+ if profile["profile_ref"].to_s.strip.empty?
392
+ diags << source_diag(SOURCE_DIAG_MISSING_PROFILE_REF,
393
+ "profile_candidate must include non-empty profile_ref")
394
+ end
395
+ unless profile["row_authority_policy"] == "pack_descriptor_rows_aggregated_by_profile"
396
+ diags << source_diag(SOURCE_DIAG_INVALID_CONFLICT_POLICY,
397
+ "profile_candidate must set row_authority_policy to " \
398
+ "'pack_descriptor_rows_aggregated_by_profile'")
399
+ end
400
+ unless profile["selected_pack_refs"].is_a?(Array) && profile["selected_pack_refs"].any?
401
+ diags << source_diag(SOURCE_DIAG_MISSING_SELECTED_PACK_REF,
402
+ "profile_candidate must include non-empty selected_pack_refs")
403
+ end
404
+ unless profile["pack_order"].is_a?(Array) && profile["pack_order"] == profile["selected_pack_refs"]
405
+ diags << source_diag(SOURCE_DIAG_INVALID_PACK_ORDER,
406
+ "profile_candidate pack_order must exactly match selected_pack_refs")
407
+ end
408
+ unless conflict_policy_rejects_duplicates?(profile["conflict_policy"])
409
+ diags << source_diag(SOURCE_DIAG_INVALID_CONFLICT_POLICY,
410
+ "profile_candidate conflict_policy must reject duplicate row ownership")
411
+ end
412
+ unless profile["pack_descriptor_candidates"].is_a?(Array)
413
+ diags << source_diag(SOURCE_DIAG_MISSING_PACK_DESCRIPTOR,
414
+ "profile_candidate must include pack_descriptor_candidates")
415
+ end
416
+ if profile.fetch("row_conflict_overrides", {}).is_a?(Hash) &&
417
+ profile.fetch("row_conflict_overrides", {}).any?
418
+ diags << source_diag(SOURCE_DIAG_PROFILE_OVERRIDE_FORBIDDEN,
419
+ "profile_candidate cannot override pack-row ownership conflicts")
420
+ end
421
+ diags
422
+ end
423
+
424
+ def profile_authority_diagnostics(profile, pack_candidates)
425
+ diags = []
426
+ selected = Array(profile["selected_pack_refs"])
427
+ pack_refs = pack_candidates.map { |pack| pack.is_a?(Hash) ? pack["pack_ref"] : nil }.compact
428
+ missing = selected - pack_refs
429
+ missing.each do |pack_ref|
430
+ diags << source_diag(SOURCE_DIAG_MISSING_SELECTED_PACK_REF,
431
+ "profile_candidate selected pack_ref #{pack_ref.inspect} was not supplied")
432
+ end
433
+ diags
434
+ end
435
+
436
+ def row_conflict_diagnostics(pack_candidates, profile)
437
+ diags = []
438
+ seen_rows = {}
439
+ seen_aliases = {}
440
+
441
+ selected_pack_refs = profile ? Array(profile["selected_pack_refs"]) : nil
442
+ selected_packs = pack_candidates.select do |pack|
443
+ pack.is_a?(Hash) && (selected_pack_refs.nil? || selected_pack_refs.include?(pack["pack_ref"]))
444
+ end
445
+
446
+ selected_packs.each do |pack|
447
+ pack_rows(pack).each do |section, row_id, row|
448
+ key = "#{section}:#{row_id}"
449
+ if seen_rows.key?(key)
450
+ diags << source_diag(SOURCE_DIAG_DUPLICATE_ROW_OWNERSHIP,
451
+ "#{key} claimed by #{seen_rows.fetch(key)} and #{pack["pack_ref"]}")
452
+ else
453
+ seen_rows[key] = pack["pack_ref"]
454
+ end
455
+
456
+ next unless section == "oof_descriptor"
457
+
458
+ Array(row["aliases"]).each do |ali|
459
+ if seen_aliases.key?(ali)
460
+ diags << source_diag(SOURCE_DIAG_DUPLICATE_ALIAS_OWNERSHIP,
461
+ "alias #{ali.inspect} claimed by #{seen_aliases.fetch(ali)} and #{pack["pack_ref"]}")
462
+ else
463
+ seen_aliases[ali] = pack["pack_ref"]
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ diags
470
+ end
471
+
472
+ def excluded_namespace_claim_diagnostics(pack_candidates)
473
+ pack_candidates.flat_map do |pack|
474
+ next [] unless pack.is_a?(Hash)
475
+
476
+ Array(pack["owned_oof_descriptors"]).flat_map do |row|
477
+ tokens = [row["code"], *Array(row["aliases"])].compact
478
+ tokens.flat_map do |token|
479
+ REQUIRED_EXCLUDED_PREFIXES.select { |prefix| token.start_with?(prefix) }.map do |prefix|
480
+ source_diag(SOURCE_DIAG_EXCLUDED_NAMESPACE_CLAIM,
481
+ "#{token.inspect} is under excluded namespace #{prefix.inspect}")
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ def derive_registry_from_profile(profile, pack_candidates)
489
+ selected = Array(profile["selected_pack_refs"])
490
+ packs_by_ref = pack_candidates.to_h { |pack| [pack.fetch("pack_ref"), pack] }
491
+ selected_packs = selected.map { |pack_ref| packs_by_ref.fetch(pack_ref) }
492
+ excluded_namespaces = profile["excluded_namespaces"] ||
493
+ profile.dig("registry", "excluded_namespaces") ||
494
+ REQUIRED_EXCLUDED_PREFIXES.map { |prefix| { "prefix" => prefix } }
495
+
496
+ {
497
+ "kind" => "oof_fragment_registry",
498
+ "format_version" => FORMAT_VERSION,
499
+ "source_authority" => source_authority_summary(profile).merge(
500
+ "profile_ref" => profile["profile_ref"],
501
+ "profile_authority_scope" => "selected_pack_set_order_conflict_policy",
502
+ "pack_row_authority_scope" => "row_identity_ownership"
503
+ ),
504
+ "historical_source_refs" => Array(profile["historical_source_refs"]),
505
+ "migration_policy" => "derived_after_profile_pack_source_acceptance",
506
+ "forward_shape_authority" => "LANG-R121-A plus LANG-R122-I1",
507
+ "oof_descriptors" => selected_packs.flat_map { |pack| annotate_source_rows(pack, "owned_oof_descriptors") },
508
+ "fragment_rows" => selected_packs.flat_map { |pack| annotate_source_rows(pack, "owned_fragment_rows") },
509
+ "support_markers" => {
510
+ "invariant_support_markers" => selected_packs.flat_map { |pack| annotate_source_rows(pack, "owned_support_markers") }
511
+ },
512
+ "excluded_namespaces" => excluded_namespaces
513
+ }
514
+ end
515
+
516
+ def annotate_source_rows(pack, key)
517
+ Array(pack[key]).map do |row|
518
+ row.merge(
519
+ "row_authority" => {
520
+ "pack_ref" => pack["pack_ref"],
521
+ "authority_kind" => pack.dig("authority", "authority_kind"),
522
+ "canon_status" => pack.dig("authority", "canon_status")
523
+ }
524
+ )
525
+ end
526
+ end
527
+
528
+ def pack_rows(pack)
529
+ [
530
+ *Array(pack["owned_oof_descriptors"]).map { |row| ["oof_descriptor", row["code"], row] },
531
+ *Array(pack["owned_fragment_rows"]).map { |row| ["fragment_row", row["name"], row] },
532
+ *Array(pack["owned_support_markers"]).map { |row| ["support_marker", row["code"], row] }
533
+ ].select { |_section, row_id, row| row_id && row.is_a?(Hash) }
534
+ end
535
+
536
+ def conflict_policy_rejects_duplicates?(policy)
537
+ return policy == "reject_duplicate_row_ownership" if policy.is_a?(String)
538
+ return false unless policy.is_a?(Hash)
539
+
540
+ %w[
541
+ duplicate_oof_descriptor
542
+ duplicate_fragment_row
543
+ duplicate_support_marker
544
+ duplicate_alias_owner
545
+ missing_selected_pack_ref
546
+ excluded_namespace
547
+ ].all? { |key| policy[key] == "reject" }
548
+ end
549
+
550
+ def source_authority_summary(source_envelope)
551
+ authority = source_envelope.is_a?(Hash) ? source_envelope["authority"] : nil
552
+ return {} unless authority.is_a?(Hash)
553
+
554
+ {
555
+ "authority_ref" => authority["authority_ref"],
556
+ "authority_kind" => authority["authority_kind"],
557
+ "canon_status" => authority["canon_status"]
558
+ }
559
+ end
560
+
561
+ def check_top_level_shape(registry)
562
+ diags = []
563
+
564
+ unless registry.is_a?(Hash)
565
+ diags << diag(DIAG_WRONG_KIND, "registry must be a Hash, got #{registry.class}")
566
+ return diags
567
+ end
568
+
569
+ if registry["kind"] != "oof_fragment_registry"
570
+ diags << diag(DIAG_WRONG_KIND,
571
+ "registry.kind must be 'oof_fragment_registry', got #{registry["kind"].inspect}")
572
+ end
573
+
574
+ %w[oof_descriptors fragment_rows support_markers excluded_namespaces].each do |section|
575
+ unless registry.key?(section)
576
+ diags << diag(DIAG_MISSING_SECTION, "registry missing required section: #{section}")
577
+ end
578
+ end
579
+
580
+ if registry.key?("support_markers")
581
+ sm = registry["support_markers"]
582
+ unless sm.is_a?(Hash) && sm.key?("invariant_support_markers") &&
583
+ sm["invariant_support_markers"].is_a?(Array)
584
+ diags << diag(DIAG_MISSING_SECTION,
585
+ "registry.support_markers.invariant_support_markers must be an Array")
586
+ end
587
+ end
588
+
589
+ diags
590
+ end
591
+
592
+ def check_oof_descriptors(descriptors, excluded_prefixes, all_canonical_codes)
593
+ diags = []
594
+ seen_codes = Set.new
595
+ seen_aliases = {} # alias → canonical code that owns it
596
+
597
+ descriptors.each do |desc|
598
+ code = desc["code"]
599
+ next unless code
600
+
601
+ # Support marker codes must not appear in oof_descriptors
602
+ if code.match?(SUPPORT_MARKER_PATTERN)
603
+ diags << diag(DIAG_SUPPORT_MARKER_IN_DESCRIPTORS,
604
+ "#{code.inspect} matches support marker pattern (PINV-*/TINV-*) and must not " \
605
+ "appear in oof_descriptors; place under support_markers.invariant_support_markers")
606
+ next
607
+ end
608
+
609
+ # Duplicate canonical code
610
+ if seen_codes.include?(code)
611
+ diags << diag(DIAG_DUPLICATE_CODE, "duplicate OOF descriptor code: #{code.inspect}")
612
+ else
613
+ seen_codes.add(code)
614
+ end
615
+
616
+ # Excluded namespace prefix
617
+ excluded_prefixes.each do |prefix|
618
+ if code.start_with?(prefix)
619
+ diags << diag(DIAG_EXCLUDED_NAMESPACE_COLLISION,
620
+ "OOF descriptor code #{code.inspect} is in excluded namespace #{prefix.inspect}")
621
+ end
622
+ end
623
+
624
+ # Alias checks.
625
+ # Rule: the same alias must not be claimed by two different descriptors.
626
+ # Note: an alias may match a canonical descriptor code when that descriptor
627
+ # is a compatibility alias (deprecated_by pointing back to this descriptor).
628
+ # This is the standard backward-compatibility pattern and is allowed.
629
+ Array(desc["aliases"]).each do |ali|
630
+ if seen_aliases.key?(ali)
631
+ diags << diag(DIAG_ALIAS_COLLISION,
632
+ "alias #{ali.inspect} claimed by both #{code.inspect} and #{seen_aliases[ali].inspect}")
633
+ else
634
+ seen_aliases[ali] = code
635
+ end
636
+ end
637
+
638
+ # Deprecated descriptors must name replacement
639
+ if desc["deprecated"]
640
+ rc = desc["replacement_code"]
641
+ db = desc["deprecated_by"]
642
+ if rc.nil? && db.nil?
643
+ diags << diag(DIAG_ALIAS_MISSING_REPLACEMENT,
644
+ "deprecated descriptor #{code.inspect} must set replacement_code or deprecated_by")
645
+ elsif rc && !all_canonical_codes.include?(rc)
646
+ diags << diag(DIAG_ALIAS_MISSING_REPLACEMENT,
647
+ "descriptor #{code.inspect} replacement_code #{rc.inspect} not found among canonical codes")
648
+ end
649
+ end
650
+ end
651
+
652
+ diags
653
+ end
654
+
655
+ def check_fragment_rows(rows)
656
+ diags = []
657
+
658
+ rows.each do |row|
659
+ name = row["name"]
660
+ next unless name
661
+
662
+ case name
663
+ when OOF_ROW_NAME
664
+ if row["loadable"] == true
665
+ diags << diag(DIAG_OOF_PROJECTION_LOADABLE,
666
+ "fragment row 'oof' must not be loadable " \
667
+ "(status-primary/status-only projection; blocked, non-loadable, capability-free)")
668
+ end
669
+ if row["capability"] == true
670
+ diags << diag(DIAG_OOF_PROJECTION_CAPABILITY,
671
+ "fragment row 'oof' must not have capability: true (capability-free by design)")
672
+ end
673
+
674
+ when *GUARDED_NON_FRAGMENT_NAMES
675
+ ck = row["classification_kind"]
676
+ unless ck == "not_fragment_class"
677
+ diags << diag(DIAG_GUARDED_NON_FRAGMENT,
678
+ "fragment row #{name.inspect} is a guarded non-fragment; " \
679
+ "classification_kind must be 'not_fragment_class', got #{ck.inspect}")
680
+ end
681
+ if row["loadable"] == true
682
+ diags << diag(DIAG_GUARDED_NON_FRAGMENT,
683
+ "guarded non-fragment row #{name.inspect} must not be loadable")
684
+ end
685
+ end
686
+ end
687
+
688
+ diags
689
+ end
690
+
691
+ def check_support_markers(markers, oof_code_set)
692
+ diags = []
693
+
694
+ markers.each do |marker|
695
+ code = marker["code"]
696
+ next unless code
697
+
698
+ # Code must not collide with any OOF descriptor code or alias
699
+ if oof_code_set.include?(code)
700
+ diags << diag(DIAG_SUPPORT_MARKER_CODE_COLLISION,
701
+ "support marker code #{code.inspect} collides with OOF descriptor code or alias; " \
702
+ "support marker codes must be distinct from public OOF codes")
703
+ end
704
+
705
+ # Must be non-public
706
+ stability = marker["public_code_stability"]
707
+ unless SUPPORT_MARKER_STABILITY_VALUES.include?(stability)
708
+ diags << diag(DIAG_SUPPORT_MARKER_PUBLIC,
709
+ "support marker #{code.inspect} has public_code_stability #{stability.inspect}; " \
710
+ "must be non_public_support_marker or proof_only (support markers are non-public)")
711
+ end
712
+
713
+ # Must not be emitted (no blocking_oof status_class, no emitted lifecycle_state)
714
+ if marker["lifecycle_state"] == "emitted" || marker["status_class"] == "blocking_oof"
715
+ diags << diag(DIAG_SUPPORT_MARKER_EMITTED,
716
+ "support marker #{code.inspect} must not be emitted as a public diagnostic; " \
717
+ "lifecycle_state/status_class must not indicate emission")
718
+ end
719
+ end
720
+
721
+ diags
722
+ end
723
+
724
+ def check_excluded_namespaces(namespaces)
725
+ diags = []
726
+ present = namespaces.map { |n| n["prefix"] }.compact.to_set
727
+
728
+ REQUIRED_EXCLUDED_PREFIXES.each do |required|
729
+ unless present.include?(required)
730
+ diags << diag(DIAG_MISSING_SECTION,
731
+ "excluded_namespaces must include required prefix #{required.inspect}; " \
732
+ "(compiler_profile_contract.* and compiler_profile_contract_refusal.* " \
733
+ "are always excluded from OOF namespace)")
734
+ end
735
+ end
736
+
737
+ diags
738
+ end
739
+
740
+ def source_diag(code, message)
741
+ { "code" => code, "message" => message }
742
+ end
743
+
744
+ def build_source_result(valid, source_mode, registry_present, source_diags, registry_validation,
745
+ source_authority = {})
746
+ {
747
+ "kind" => "oof_fragment_registry_source_validation",
748
+ "format_version" => "0.1.0",
749
+ "valid" => valid,
750
+ "source_mode" => source_mode,
751
+ "registry_present" => registry_present,
752
+ "source_authority" => source_authority,
753
+ "source_diagnostics" => source_diags,
754
+ "registry_validation" => registry_validation,
755
+ "closed_surface_assertions" => {
756
+ "static_data_file" => false,
757
+ "lib_igniter_lang_rb_require" => false,
758
+ "compiler_pass_integration" => false,
759
+ "public_api_cli" => false,
760
+ "top_level_report_diagnostics" => false,
761
+ "compiler_result_field" => false,
762
+ "loader_report" => false,
763
+ "compatibility_report" => false,
764
+ "runtime_behavior" => false,
765
+ "igapp_mutation" => false,
766
+ "specs_canon_proposals" => false
767
+ }
768
+ }
769
+ end
770
+
771
+ def diag(code, message)
772
+ { "code" => code, "message" => message }
773
+ end
774
+
775
+ def build_result(valid, diags, inactive_rows)
776
+ {
777
+ "kind" => "oof_fragment_registry_validation",
778
+ "format_version" => FORMAT_VERSION,
779
+ "valid" => valid,
780
+ "registry_service_present" => true,
781
+ "checked_sections" => %w[
782
+ oof_descriptors
783
+ fragment_rows
784
+ support_markers.invariant_support_markers
785
+ excluded_namespaces
786
+ ],
787
+ "diagnostics" => diags,
788
+ "inactive_rows" => inactive_rows,
789
+ "closed_surface_assertions" => {
790
+ "compiler_integration" => false,
791
+ "public_api_cli" => false,
792
+ "top_level_report_diagnostics" => false,
793
+ "compiler_result_field" => false,
794
+ "loader_report" => false,
795
+ "compatibility_report" => false,
796
+ "runtime_behavior" => false,
797
+ "igapp_mutation" => false
798
+ }
799
+ }
800
+ end
801
+ end
802
+ end