file-brief 2.0.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.
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env Rscript
2
+ #
3
+ # =============================================================================
4
+ # 代码介绍
5
+ # =============================================================================
6
+ # 输入:
7
+ # 1. 命令行第一个参数:一个 .rds、.rda 或 .RData 文件的绝对路径。
8
+ # 2. 文件内容必须能够由 base R 的 readRDS() 或 load() 读取。
9
+ #
10
+ # 输出:
11
+ # 标准输出为 UTF-8 JSON。顶层包含 status、format、objects 和 warnings。
12
+ # objects 只描述对象名称、class、typeof、维度、列名、缺失量、近似唯一值数量、
13
+ # 列表成员名、函数参数名等结构信息;不会输出数据行、单元格值、因子水平或文本内容。
14
+ # 失败时仍输出 status="error" 的 JSON,并以非零状态退出。
15
+ #
16
+ # 作用:
17
+ # 为 file-brief 技能提供稳定的 R 数据结构探查能力,使 Agent 不必在每个
18
+ # 分析任务中重复编写 readRDS()/load()/str() 等一次性检查代码。
19
+ #
20
+ # 设计逻辑:
21
+ # - 根据扩展名选择 readRDS() 或隔离环境中的 load()。
22
+ # - 递归描述对象,但限制最大深度、成员数量和统计样本量,避免巨大对象产生巨大输出。
23
+ # - 数据框按列输出结构统计;数组输出维度;列表输出成员结构;其他对象输出通用元数据。
24
+ # - 所有 JSON 由 jsonlite 生成,保证 Python 调用方可稳定解析。
25
+ #
26
+ # 主要函数:
27
+ # scalar_text() 将 class/typeof 等结构标签压缩为单个字符串。
28
+ # approximate_unique() 在最多 10,000 个元素上计算近似唯一值数量。
29
+ # summarize_column() 描述数据框的一列,不泄露实际值。
30
+ # summarize_object() 递归描述任意 R 对象。
31
+ # inspect_r_file() 读取文件并构造最终结构结果。
32
+ #
33
+ # 调用方式:
34
+ # Rscript --vanilla inspect_r_data.R "/path/to/data.rds"
35
+ # =============================================================================
36
+
37
+ suppressWarnings(suppressMessages({
38
+ if (!requireNamespace("jsonlite", quietly = TRUE)) {
39
+ stop("The jsonlite package is required.")
40
+ }
41
+ }))
42
+
43
+ MAX_DEPTH <- 3L
44
+ MAX_CHILDREN <- 100L
45
+ MAX_UNIQUE_SAMPLE <- 10000L
46
+
47
+ scalar_text <- function(value) {
48
+ if (length(value) == 0L) {
49
+ return("")
50
+ }
51
+ paste(as.character(value), collapse = ", ")
52
+ }
53
+
54
+ approximate_unique <- function(value) {
55
+ if (length(value) == 0L) {
56
+ return(0L)
57
+ }
58
+ sampled <- head(value, MAX_UNIQUE_SAMPLE)
59
+ sampled <- sampled[!is.na(sampled)]
60
+ length(unique(sampled))
61
+ }
62
+
63
+ summarize_column <- function(value) {
64
+ list(
65
+ class = scalar_text(class(value)),
66
+ typeof = typeof(value),
67
+ length = length(value),
68
+ missing = sum(is.na(value)),
69
+ approximate_unique = approximate_unique(value),
70
+ unique_sample_limit = min(length(value), MAX_UNIQUE_SAMPLE)
71
+ )
72
+ }
73
+
74
+ summarize_object <- function(value, depth = 0L) {
75
+ base <- list(
76
+ class = scalar_text(class(value)),
77
+ typeof = typeof(value),
78
+ length = length(value),
79
+ object_size_bytes = as.numeric(utils::object.size(value))
80
+ )
81
+
82
+ dims <- dim(value)
83
+ if (!is.null(dims)) {
84
+ base$dimensions <- as.integer(dims)
85
+ }
86
+
87
+ if (is.data.frame(value)) {
88
+ column_names <- names(value)
89
+ selected <- head(seq_along(value), MAX_CHILDREN)
90
+ columns <- lapply(selected, function(index) summarize_column(value[[index]]))
91
+ names(columns) <- column_names[selected]
92
+ base$column_count <- ncol(value)
93
+ base$row_count <- nrow(value)
94
+ base$columns <- columns
95
+ base$truncated_columns <- ncol(value) > MAX_CHILDREN
96
+ return(base)
97
+ }
98
+
99
+ if (is.matrix(value) || is.array(value)) {
100
+ base$missing <- sum(is.na(value))
101
+ return(base)
102
+ }
103
+
104
+ if (is.function(value)) {
105
+ base$parameters <- names(formals(value))
106
+ return(base)
107
+ }
108
+
109
+ if (isS4(value)) {
110
+ slot_names <- methods::slotNames(value)
111
+ base$slot_names <- head(slot_names, MAX_CHILDREN)
112
+ if (depth < MAX_DEPTH) {
113
+ chosen <- head(slot_names, MAX_CHILDREN)
114
+ slots <- lapply(chosen, function(slot_name) {
115
+ summarize_object(methods::slot(value, slot_name), depth + 1L)
116
+ })
117
+ names(slots) <- chosen
118
+ base$slots <- slots
119
+ }
120
+ base$truncated_slots <- length(slot_names) > MAX_CHILDREN
121
+ return(base)
122
+ }
123
+
124
+ if (is.list(value)) {
125
+ item_names <- names(value)
126
+ if (is.null(item_names)) {
127
+ item_names <- paste0("[[", seq_along(value), "]]")
128
+ } else {
129
+ empty <- !nzchar(item_names)
130
+ item_names[empty] <- paste0("[[", which(empty), "]]")
131
+ }
132
+ chosen_indices <- head(seq_along(value), MAX_CHILDREN)
133
+ base$member_names <- item_names[chosen_indices]
134
+ if (depth < MAX_DEPTH) {
135
+ children <- lapply(chosen_indices, function(index) {
136
+ summarize_object(value[[index]], depth + 1L)
137
+ })
138
+ names(children) <- item_names[chosen_indices]
139
+ base$members <- children
140
+ }
141
+ base$truncated_members <- length(value) > MAX_CHILDREN
142
+ return(base)
143
+ }
144
+
145
+ if (is.atomic(value)) {
146
+ base$missing <- sum(is.na(value))
147
+ base$approximate_unique <- approximate_unique(value)
148
+ base$unique_sample_limit <- min(length(value), MAX_UNIQUE_SAMPLE)
149
+ }
150
+
151
+ base
152
+ }
153
+
154
+ inspect_r_file <- function(path) {
155
+ extension <- tolower(tools::file_ext(path))
156
+ warnings <- character()
157
+
158
+ if (extension == "rds") {
159
+ value <- readRDS(path)
160
+ objects <- list(value = summarize_object(value))
161
+ return(list(
162
+ status = "ok",
163
+ format = "RDS",
164
+ objects = objects,
165
+ warnings = warnings
166
+ ))
167
+ }
168
+
169
+ if (extension %in% c("rda", "rdata")) {
170
+ environment <- new.env(parent = emptyenv())
171
+ object_names <- load(path, envir = environment)
172
+ selected_names <- head(object_names, MAX_CHILDREN)
173
+ objects <- lapply(selected_names, function(object_name) {
174
+ summarize_object(get(object_name, envir = environment, inherits = FALSE))
175
+ })
176
+ names(objects) <- selected_names
177
+ if (length(object_names) > MAX_CHILDREN) {
178
+ warnings <- c(warnings, sprintf(
179
+ "Only the first %d of %d objects were described.",
180
+ MAX_CHILDREN,
181
+ length(object_names)
182
+ ))
183
+ }
184
+ return(list(
185
+ status = "ok",
186
+ format = "RData",
187
+ object_count = length(object_names),
188
+ objects = objects,
189
+ warnings = warnings
190
+ ))
191
+ }
192
+
193
+ stop(sprintf("Unsupported R data extension: %s", extension))
194
+ }
195
+
196
+ args <- commandArgs(trailingOnly = TRUE)
197
+ if (length(args) != 1L) {
198
+ cat(jsonlite::toJSON(
199
+ list(status = "error", message = "Expected exactly one input path."),
200
+ auto_unbox = TRUE,
201
+ null = "null"
202
+ ))
203
+ quit(status = 2L)
204
+ }
205
+
206
+ input_path <- normalizePath(args[[1L]], winslash = "\\", mustWork = FALSE)
207
+
208
+ tryCatch(
209
+ {
210
+ if (!file.exists(input_path)) {
211
+ stop(sprintf("Input file does not exist: %s", input_path))
212
+ }
213
+ result <- inspect_r_file(input_path)
214
+ cat(jsonlite::toJSON(
215
+ result,
216
+ auto_unbox = TRUE,
217
+ null = "null",
218
+ digits = NA
219
+ ))
220
+ },
221
+ error = function(error) {
222
+ cat(jsonlite::toJSON(
223
+ list(status = "error", message = conditionMessage(error)),
224
+ auto_unbox = TRUE,
225
+ null = "null"
226
+ ))
227
+ quit(status = 2L)
228
+ }
229
+ )