altium-toolkit 1.0.10 → 1.1.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 (63) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
  11. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  12. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  13. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  14. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  15. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  16. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  17. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  18. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  19. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  20. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  21. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  22. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  23. package/package.json +1 -1
  24. package/src/core/altium/AltiumParser.mjs +7 -2
  25. package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
  26. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  27. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  28. package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
  29. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  30. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  31. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  32. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  33. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  34. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  35. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  36. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  37. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  38. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  39. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  40. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  41. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  42. package/src/core/altium/PcbModelParser.mjs +182 -18
  43. package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
  44. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  45. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  46. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  47. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  48. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  49. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  50. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  51. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  52. package/src/core/altium/PrjPcbModelParser.mjs +45 -0
  53. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  54. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  55. package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
  56. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  57. package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
  58. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
  59. package/src/parser.mjs +15 -0
  60. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  61. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  62. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  63. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -0,0 +1,263 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds PCB-only BOM grouping and parameter-normalization profiles.
7
+ */
8
+ export class PcbBomProfileBuilder {
9
+ static SCHEMA_ID = 'altium-toolkit.pcb.bom-profile.a1'
10
+
11
+ /**
12
+ * Builds a deterministic BOM profile from PCB component rows.
13
+ * @param {object[]} components Normalized PCB component rows.
14
+ * @param {{ source?: string }} options Build options.
15
+ * @returns {object}
16
+ */
17
+ static build(components, options = {}) {
18
+ const normalizedComponents = (components || []).map((component) =>
19
+ PcbBomProfileBuilder.#component(component)
20
+ )
21
+ const included = normalizedComponents.filter(
22
+ (component) => component.includeInBom
23
+ )
24
+ const groups = PcbBomProfileBuilder.#groups(included)
25
+ const exclusions = normalizedComponents
26
+ .filter((component) => !component.includeInBom)
27
+ .map((component) => ({
28
+ designator: component.designator,
29
+ reason: 'component-kind:no-bom'
30
+ }))
31
+
32
+ return {
33
+ schema: PcbBomProfileBuilder.SCHEMA_ID,
34
+ source: String(options.source || 'pcb-document'),
35
+ summary: {
36
+ componentCount: normalizedComponents.length,
37
+ includedComponentCount: included.length,
38
+ excludedComponentCount: exclusions.length,
39
+ groupCount: groups.length,
40
+ normalizedParameterCount:
41
+ PcbBomProfileBuilder.#normalizedParameterCount(included)
42
+ },
43
+ groups,
44
+ components: normalizedComponents,
45
+ exclusions
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Normalizes one component into a BOM profile row.
51
+ * @param {object} component Source component.
52
+ * @returns {object}
53
+ */
54
+ static #component(component) {
55
+ const normalizedParameters = PcbBomProfileBuilder.#normalizedParameters(
56
+ component?.parameters || {}
57
+ )
58
+ const sourceParameterNames = Object.keys(component?.parameters || {})
59
+ const includeInBom =
60
+ component?.componentKind?.includeInBom === false ? false : true
61
+ const value =
62
+ normalizedParameters.comment ||
63
+ component?.description ||
64
+ component?.value ||
65
+ component?.pattern ||
66
+ ''
67
+
68
+ return {
69
+ designator: String(component?.designator || ''),
70
+ includeInBom,
71
+ componentKind: component?.componentKind?.name || 'standard',
72
+ pattern: String(component?.pattern || ''),
73
+ source: String(component?.source || ''),
74
+ value,
75
+ normalizedParameters,
76
+ sourceParameterNames
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Builds grouped BOM rows from included components.
82
+ * @param {object[]} components Included normalized components.
83
+ * @returns {object[]}
84
+ */
85
+ static #groups(components) {
86
+ const byKey = new Map()
87
+
88
+ for (const component of components || []) {
89
+ const key = PcbBomProfileBuilder.#groupKey(component)
90
+ if (!byKey.has(key)) {
91
+ byKey.set(key, {
92
+ key,
93
+ quantity: 0,
94
+ designators: [],
95
+ pattern: component.pattern,
96
+ source: component.source,
97
+ value: component.value,
98
+ normalizedParameters: {}
99
+ })
100
+ }
101
+ const group = byKey.get(key)
102
+ group.quantity += 1
103
+ group.designators.push(component.designator)
104
+ group.normalizedParameters = PcbBomProfileBuilder.#mergeParameters(
105
+ group.normalizedParameters,
106
+ component.normalizedParameters
107
+ )
108
+ }
109
+
110
+ return [...byKey.values()].map((group) => ({
111
+ ...group,
112
+ designators: PcbBomProfileBuilder.#sortDesignators(
113
+ group.designators
114
+ ),
115
+ normalizedParameters:
116
+ PcbBomProfileBuilder.#stripGroupingOnlyParameters(
117
+ group.normalizedParameters
118
+ )
119
+ }))
120
+ }
121
+
122
+ /**
123
+ * Builds the stable grouping key for one included component.
124
+ * @param {object} component Normalized component row.
125
+ * @returns {string}
126
+ */
127
+ static #groupKey(component) {
128
+ const parameters = component.normalizedParameters || {}
129
+ return [
130
+ parameters.manufacturer || '',
131
+ parameters.manufacturerPartNumber || '',
132
+ parameters.supplierPartNumber || '',
133
+ component.pattern || '',
134
+ component.value || ''
135
+ ].join('|')
136
+ }
137
+
138
+ /**
139
+ * Converts source parameters into normalized BOM aliases.
140
+ * @param {Record<string, string>} parameters Source parameters.
141
+ * @returns {Record<string, string>}
142
+ */
143
+ static #normalizedParameters(parameters) {
144
+ const normalized = {}
145
+
146
+ for (const [name, value] of Object.entries(parameters || {})) {
147
+ const text = String(value || '').trim()
148
+ if (!text) continue
149
+
150
+ const alias = PcbBomProfileBuilder.#aliasForName(name)
151
+ if (alias && !normalized[alias]) {
152
+ normalized[alias] = text
153
+ }
154
+ if (
155
+ alias === 'supplierPartNumber' &&
156
+ /jlcpcb/iu.test(String(name || '')) &&
157
+ !normalized.supplier
158
+ ) {
159
+ normalized.supplier = 'JLCPCB'
160
+ }
161
+ }
162
+
163
+ return normalized
164
+ }
165
+
166
+ /**
167
+ * Resolves a normalized alias for a source parameter name.
168
+ * @param {string} name Source parameter name.
169
+ * @returns {string}
170
+ */
171
+ static #aliasForName(name) {
172
+ const key = String(name || '')
173
+ .toLowerCase()
174
+ .replace(/[^a-z0-9]+/gu, '')
175
+
176
+ if (['manufacturer', 'mfr', 'mfg'].includes(key)) {
177
+ return 'manufacturer'
178
+ }
179
+ if (
180
+ [
181
+ 'manufacturerpartnumber',
182
+ 'manufacturerpn',
183
+ 'mfrpartnumber',
184
+ 'mfgpartnumber',
185
+ 'mpn'
186
+ ].includes(key)
187
+ ) {
188
+ return 'manufacturerPartNumber'
189
+ }
190
+ if (
191
+ [
192
+ 'supplierpartnumber',
193
+ 'supplierpn',
194
+ 'supplierpart',
195
+ 'jlcpcbpart',
196
+ 'jlcpcbpartnumber',
197
+ 'jlcpcbpartno'
198
+ ].includes(key)
199
+ ) {
200
+ return 'supplierPartNumber'
201
+ }
202
+ if (key === 'category') return 'category'
203
+ if (key === 'comment' || key === 'value') return 'comment'
204
+
205
+ return ''
206
+ }
207
+
208
+ /**
209
+ * Merges normalized parameters while preserving the first non-empty value.
210
+ * @param {object} existing Existing values.
211
+ * @param {object} incoming Candidate values.
212
+ * @returns {object}
213
+ */
214
+ static #mergeParameters(existing, incoming) {
215
+ const merged = { ...(existing || {}) }
216
+
217
+ for (const [key, value] of Object.entries(incoming || {})) {
218
+ if (merged[key] || value === undefined || value === '') continue
219
+ merged[key] = value
220
+ }
221
+
222
+ return merged
223
+ }
224
+
225
+ /**
226
+ * Removes fields that are component-specific rather than group identity.
227
+ * @param {object} parameters Normalized parameters.
228
+ * @returns {object}
229
+ */
230
+ static #stripGroupingOnlyParameters(parameters) {
231
+ const { comment: _comment, ...rest } = parameters || {}
232
+ return rest
233
+ }
234
+
235
+ /**
236
+ * Counts normalized source aliases used for group identity.
237
+ * @param {object[]} components Included component rows.
238
+ * @returns {number}
239
+ */
240
+ static #normalizedParameterCount(components) {
241
+ return (components || []).reduce(
242
+ (count, component) =>
243
+ count +
244
+ Object.keys(component.normalizedParameters || {}).filter(
245
+ (key) => !['comment', 'supplier'].includes(key)
246
+ ).length,
247
+ 0
248
+ )
249
+ }
250
+
251
+ /**
252
+ * Sorts designators in a stable natural-ish order.
253
+ * @param {string[]} designators Designator list.
254
+ * @returns {string[]}
255
+ */
256
+ static #sortDesignators(designators) {
257
+ return (designators || []).slice().sort((left, right) =>
258
+ String(left).localeCompare(String(right), undefined, {
259
+ numeric: true
260
+ })
261
+ )
262
+ }
263
+ }
@@ -0,0 +1,146 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ const { getField, parseNumericField } = ParserUtils
8
+
9
+ const KIND_BY_VALUE = {
10
+ 0: {
11
+ name: 'standard',
12
+ displayName: 'Standard',
13
+ includeInBom: true,
14
+ includeInNetlist: true,
15
+ includeInPnp: true
16
+ },
17
+ 1: {
18
+ name: 'mechanical',
19
+ displayName: 'Mechanical',
20
+ includeInBom: true,
21
+ includeInNetlist: true,
22
+ includeInPnp: true
23
+ },
24
+ 2: {
25
+ name: 'graphical',
26
+ displayName: 'Graphical',
27
+ includeInBom: false,
28
+ includeInNetlist: false,
29
+ includeInPnp: false
30
+ },
31
+ 3: {
32
+ name: 'net-tie-bom',
33
+ displayName: 'Net Tie BOM',
34
+ includeInBom: true,
35
+ includeInNetlist: true,
36
+ includeInPnp: true
37
+ },
38
+ 4: {
39
+ name: 'net-tie-no-bom',
40
+ displayName: 'Net Tie No BOM',
41
+ includeInBom: false,
42
+ includeInNetlist: true,
43
+ includeInPnp: true
44
+ },
45
+ 5: {
46
+ name: 'standard-no-bom',
47
+ displayName: 'Standard No BOM',
48
+ includeInBom: false,
49
+ includeInNetlist: true,
50
+ includeInPnp: true
51
+ },
52
+ 6: {
53
+ name: 'jumper',
54
+ displayName: 'Jumper',
55
+ includeInBom: true,
56
+ includeInNetlist: true,
57
+ includeInPnp: true
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Normalizes native PCB component kind fields and participation policy.
63
+ */
64
+ export class PcbComponentKindPolicy {
65
+ /**
66
+ * Parses native versioned component-kind fields.
67
+ * @param {Record<string, string | string[]>} fields Native component row.
68
+ * @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPnp: boolean } | undefined}
69
+ */
70
+ static parse(fields) {
71
+ if (!PcbComponentKindPolicy.#hasKindField(fields)) return undefined
72
+
73
+ const value = PcbComponentKindPolicy.#kindValue(fields)
74
+ const policy = KIND_BY_VALUE[value] || {
75
+ name: 'unknown',
76
+ displayName: 'Unknown',
77
+ includeInBom: true,
78
+ includeInNetlist: true,
79
+ includeInPnp: true
80
+ }
81
+
82
+ return {
83
+ value,
84
+ ...policy
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Returns true when a component row carries any native kind field.
90
+ * @param {Record<string, string | string[]>} fields Native component row.
91
+ * @returns {boolean}
92
+ */
93
+ static #hasKindField(fields) {
94
+ return [
95
+ 'COMPONENTKIND',
96
+ 'ComponentKind',
97
+ 'COMPONENTKINDVERSION2',
98
+ 'ComponentKindVersion2',
99
+ 'COMPONENTKINDVERSION3',
100
+ 'ComponentKindVersion3'
101
+ ].some((key) => Object.hasOwn(fields || {}, key))
102
+ }
103
+
104
+ /**
105
+ * Resolves the effective native component kind from versioned fields.
106
+ * @param {Record<string, string | string[]>} fields Native component row.
107
+ * @returns {number}
108
+ */
109
+ static #kindValue(fields) {
110
+ const v1 = PcbComponentKindPolicy.#numericField(fields, [
111
+ 'COMPONENTKIND',
112
+ 'ComponentKind'
113
+ ])
114
+ const v2 = PcbComponentKindPolicy.#numericField(fields, [
115
+ 'COMPONENTKINDVERSION2',
116
+ 'ComponentKindVersion2'
117
+ ])
118
+ const v3 = PcbComponentKindPolicy.#numericField(fields, [
119
+ 'COMPONENTKINDVERSION3',
120
+ 'ComponentKindVersion3'
121
+ ])
122
+
123
+ if (v3 === 6) return v3
124
+ if (v2 !== null && v2 >= 5) return v2
125
+ return v1 ?? 0
126
+ }
127
+
128
+ /**
129
+ * Returns the first finite numeric value from possible field names.
130
+ * @param {Record<string, string | string[]>} fields Native fields.
131
+ * @param {string[]} keys Candidate keys.
132
+ * @returns {number | null}
133
+ */
134
+ static #numericField(fields, keys) {
135
+ for (const key of keys) {
136
+ const value = parseNumericField(fields, key)
137
+ if (Number.isFinite(value)) return value
138
+
139
+ const raw = getField(fields, key)
140
+ const parsed = Number.parseInt(String(raw ?? '').trim(), 10)
141
+ if (Number.isFinite(parsed)) return parsed
142
+ }
143
+
144
+ return null
145
+ }
146
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Classifies layer-stack source evidence and regeneration limits.
7
+ */
8
+ export class PcbLayerStackFidelityReportBuilder {
9
+ static SCHEMA_ID = 'altium-toolkit.pcb.layer-stack-fidelity.a1'
10
+
11
+ /**
12
+ * Builds a source-fidelity report for a layer-stack read model.
13
+ * @param {object} readModel Layer-stack read model.
14
+ * @returns {object}
15
+ */
16
+ static build(readModel) {
17
+ const semanticSections =
18
+ PcbLayerStackFidelityReportBuilder.#semanticSections(readModel)
19
+ const nativeCacheSections =
20
+ PcbLayerStackFidelityReportBuilder.#nativeCacheSections(readModel)
21
+ const interchangeOnlySections =
22
+ PcbLayerStackFidelityReportBuilder.#interchangeOnlySections(
23
+ readModel
24
+ )
25
+ const diagnostics = readModel?.diagnostics || []
26
+ const unsupportedRegeneration =
27
+ PcbLayerStackFidelityReportBuilder.#unsupportedRegeneration(
28
+ nativeCacheSections,
29
+ diagnostics
30
+ )
31
+
32
+ return {
33
+ schema: PcbLayerStackFidelityReportBuilder.SCHEMA_ID,
34
+ sourceDocument: String(readModel?.source?.fileName || ''),
35
+ summary: {
36
+ semanticLayerCount: (readModel?.layers || []).length,
37
+ nativeCacheFeatureCount: nativeCacheSections.length,
38
+ interchangeOnlyFeatureCount: interchangeOnlySections.length,
39
+ unsupportedRegenerationCount: unsupportedRegeneration.length,
40
+ diagnosticCount: diagnostics.length
41
+ },
42
+ capabilities: {
43
+ semanticRead: semanticSections.length > 0,
44
+ nativeCacheRead: nativeCacheSections.length > 0,
45
+ interchangeRead: interchangeOnlySections.length > 0,
46
+ deterministicReport: true,
47
+ nativeRegeneration: false
48
+ },
49
+ semanticSections,
50
+ nativeCacheSections,
51
+ interchangeOnlySections,
52
+ unsupportedRegeneration,
53
+ diagnostics
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Lists semantic layer-stack sections in the model.
59
+ * @param {object} readModel Layer-stack read model.
60
+ * @returns {string[]}
61
+ */
62
+ static #semanticSections(readModel) {
63
+ return [
64
+ ['layers', readModel?.layers],
65
+ ['substacks', readModel?.substacks],
66
+ ['branches', readModel?.branches],
67
+ ['impedanceProfiles', readModel?.impedanceProfiles]
68
+ ]
69
+ .filter(([, values]) => (values || []).length > 0)
70
+ .map(([section]) => section)
71
+ }
72
+
73
+ /**
74
+ * Lists native-cache-only sections represented in the model.
75
+ * @param {object} readModel Layer-stack read model.
76
+ * @returns {string[]}
77
+ */
78
+ static #nativeCacheSections(readModel) {
79
+ const sections = []
80
+ const sourceMap = readModel?.sourceMap || {}
81
+
82
+ if (sourceMap.registryEntryCount > 0) sections.push('source.registry')
83
+ if (sourceMap.sourceKeyCount > 0) sections.push('source.keys')
84
+ if ((readModel?.topLevelBendLines || []).length > 0) {
85
+ sections.push('bendLines')
86
+ }
87
+ if (sourceMap.cavityRegionCount > 0) sections.push('cavities')
88
+ if (sourceMap.surfaceFinishCount > 0) sections.push('surfaceFinish')
89
+
90
+ return sections
91
+ }
92
+
93
+ /**
94
+ * Lists fields that are preserved as interchange-specific metadata.
95
+ * @param {object} readModel Layer-stack read model.
96
+ * @returns {string[]}
97
+ */
98
+ static #interchangeOnlySections(readModel) {
99
+ const layers = readModel?.layers || []
100
+ const sections = []
101
+
102
+ if (layers.some((layer) => layer.stackupxShared !== undefined)) {
103
+ sections.push('layers.stackupxShared')
104
+ }
105
+ if (
106
+ layers.some(
107
+ (layer) =>
108
+ Object.keys(layer.stackupxProperties || {}).length > 0
109
+ )
110
+ ) {
111
+ sections.push('layers.stackupxProperties')
112
+ }
113
+
114
+ return sections
115
+ }
116
+
117
+ /**
118
+ * Describes why native stack regeneration is intentionally unsupported.
119
+ * @param {string[]} nativeCacheSections Native cache sections.
120
+ * @param {object[]} diagnostics Read-model diagnostics.
121
+ * @returns {object[]}
122
+ */
123
+ static #unsupportedRegeneration(nativeCacheSections, diagnostics) {
124
+ const issues = []
125
+
126
+ if (nativeCacheSections.length > 0) {
127
+ issues.push({
128
+ section: 'native-cache',
129
+ reason: 'Native cache metadata is preserved for review but not regenerated.'
130
+ })
131
+ }
132
+ if ((diagnostics || []).length > 0) {
133
+ issues.push({
134
+ section: 'diagnostics',
135
+ reason: 'Unresolved references prevent equivalent native regeneration.'
136
+ })
137
+ }
138
+
139
+ return issues
140
+ }
141
+ }