Compare commits
3 Commits
624011da51
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f7aa5aaef | |||
| 8c3a10e35e | |||
| 03dbc66c0c |
+2
-8
@@ -1,8 +1,2 @@
|
||||
# Zig build artifacts
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
||||
# Local editor/OS noise
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
/.zig-cache/
|
||||
/zig-out/
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Overview
|
||||
|
||||
`facts` is a tiny compiled Zig CLI for turning file paths into stable TSV file
|
||||
metadata. It exists as an agent-safe replacement for the safe subset of `stat`,
|
||||
`du`, and `find -printf` when allowlist policy needs argv-obvious behavior.
|
||||
|
||||
## How to build
|
||||
|
||||
```sh
|
||||
zig build
|
||||
```
|
||||
|
||||
## How to test
|
||||
|
||||
```sh
|
||||
zig build test
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- One source file: `facts.zig`.
|
||||
- No runtime dependencies, no shell execution, no mutation.
|
||||
- stdout is TSV with a header; diagnostics go to stderr.
|
||||
- No filtering, sorting, paging, JSON, color, expression language, callbacks, or
|
||||
hidden state.
|
||||
- This is an inspection primitive. Keep filtering in adjacent stream tools such
|
||||
as `grep`/`pick`, not in `facts`.
|
||||
|
||||
## Requirement Governance
|
||||
|
||||
`REQUIREMENTS.md` is the requirement authority for this repository and is checked
|
||||
with `redgate`. Before implementation work, read the applicable requirement row;
|
||||
if none covers durable behavior, update `REQUIREMENTS.md` first or stop and ask.
|
||||
For behavior intended to stick, add or update a failing smoke/contract proof
|
||||
before implementation unless that would be fake or disproportionate. Before
|
||||
handoff, compare the user request and diff against `REQUIREMENTS.md`; report
|
||||
`REQUIREMENT IMPACT: updated <rows>` or exactly
|
||||
`REQUIREMENT IMPACT: none - <specific reason>`.
|
||||
@@ -0,0 +1,10 @@
|
||||
# facts — migrated
|
||||
|
||||
The canonical `facts` agent tool moved to `/opt/repositories/toolset`.
|
||||
|
||||
This repository no longer builds or releases an executable. Its Git history is the migration record; source, requirements, tests, installation, and release authority now live in toolset.
|
||||
|
||||
```sh
|
||||
cd /opt/repositories/toolset
|
||||
zig build install-text -p "$HOME/.ink"
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
# REQUIREMENTS
|
||||
|
||||
## facts
|
||||
|
||||
ring id requirement
|
||||
0 001 `facts` must be a compiled single-file Zig CLI with no runtime dependencies beyond the OS.
|
||||
0 002 `facts` must inspect file metadata only; it must not mutate files, execute callbacks, access the network, read hidden config, prompt, page, colorize, or emit JSON by default.
|
||||
1 003 `facts PATH...` must emit one TSV record per path plus a header row with stable columns `path`, `kind`, `size`, `mode`, and `mtime_ns`.
|
||||
1 003.tsv-safety `facts` must preserve one-line TSV records by rejecting unsupported path bytes that cannot be emitted raw in TSV fields, including tab, carriage return, and newline, before writing a path record to stdout.
|
||||
1 004 With no path arguments, `facts` must read one path per stdin line and emit the same TSV contract.
|
||||
1 004.streaming Stdin mode must stream with bounded memory: emit the TSV header before waiting for stdin EOF, process completed input lines independently, flush each completed output record before EOF, and never buffer all stdin.
|
||||
1 005 Per-path runtime errors must be diagnostics on stderr, must not corrupt stdout records for other paths, and must make the process exit nonzero after flushing valid records.
|
||||
1 006 `facts` must not filter, sort, glob, or provide an expression language; callers compose with `files`, `grep`, `pick`, `sort`, or similar stream tools.
|
||||
1 007 `zig build test` must prove argv input, stdin input, streaming stdin behavior, TSV header shape, TSV safety rejection, size reporting, directory reporting, help output, and per-path error handling.
|
||||
1 008 `facts --help` and `facts -h` must emit usage text to stdout without a TSV header and exit zero.
|
||||
@@ -1,16 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const exe = b.addExecutable(.{ .name = "facts", .root_module = b.createModule(.{ .root_source_file = b.path("facts.zig"), .target = target, .optimize = optimize }) });
|
||||
b.installArtifact(exe);
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
b.step("run", "Run facts").dependOn(&run_cmd.step);
|
||||
const smoke = b.addSystemCommand(&.{ "sh", "smoke.sh", "zig-out/bin/facts" });
|
||||
smoke.setCwd(b.path("."));
|
||||
smoke.step.dependOn(b.getInstallStep());
|
||||
b.step("test", "Run smoke test").dependOn(&smoke.step);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
fn usage(file: std.Io.File, io: std.Io) !void {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var w = file.writer(io, &buf);
|
||||
try w.interface.writeAll(
|
||||
\\usage: facts [PATH...]
|
||||
\\
|
||||
\\Print file metadata as TSV. With no PATH args, reads one path per stdin line.
|
||||
\\Columns: path kind size mode mtime_ns
|
||||
\\
|
||||
\\stdout is records only; diagnostics go to stderr.
|
||||
\\No filtering, sorting, deletion, exec, expression language, JSON, color, or hidden writes.
|
||||
\\
|
||||
\\examples:
|
||||
\\ files src | facts | pick --header path size
|
||||
\\ facts README.md build.zig
|
||||
\\
|
||||
);
|
||||
try w.interface.flush();
|
||||
}
|
||||
|
||||
fn die(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
|
||||
var buf: [512]u8 = undefined;
|
||||
var w = std.Io.File.stderr().writer(io, &buf);
|
||||
w.interface.print(fmt, args) catch {};
|
||||
w.interface.flush() catch {};
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
fn isHelp(arg: []const u8) bool {
|
||||
return std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h");
|
||||
}
|
||||
|
||||
fn kindName(kind: std.Io.File.Kind) []const u8 {
|
||||
return switch (kind) {
|
||||
.file => "file",
|
||||
.directory => "dir",
|
||||
.sym_link => "symlink",
|
||||
.block_device => "block",
|
||||
.character_device => "char",
|
||||
.named_pipe => "fifo",
|
||||
.unix_domain_socket => "socket",
|
||||
.whiteout => "whiteout",
|
||||
.door => "door",
|
||||
.event_port => "eventport",
|
||||
.unknown => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn modeValue(stat: std.Io.File.Stat) u64 {
|
||||
if (@hasDecl(std.Io.File.Permissions, "toMode")) return @intCast(stat.permissions.toMode());
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn printError(io: std.Io, path: []const u8, err: anyerror) !void {
|
||||
var err_buf: [512]u8 = undefined;
|
||||
var errw = std.Io.File.stderr().writer(io, &err_buf);
|
||||
try errw.interface.print("facts: {s}: {s}\n", .{ path, @errorName(err) });
|
||||
try errw.interface.flush();
|
||||
}
|
||||
|
||||
fn unsupportedPathByte(path: []const u8) ?[]const u8 {
|
||||
for (path) |byte| switch (byte) {
|
||||
'\t' => return "tab",
|
||||
'\n' => return "newline",
|
||||
'\r' => return "carriage return",
|
||||
else => {},
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
fn printUnsupportedPath(io: std.Io, byte_name: []const u8) !void {
|
||||
var err_buf: [512]u8 = undefined;
|
||||
var errw = std.Io.File.stderr().writer(io, &err_buf);
|
||||
try errw.interface.print("facts: unsupported path: contains {s}\n", .{byte_name});
|
||||
try errw.interface.flush();
|
||||
}
|
||||
|
||||
fn printFact(io: std.Io, writer: *std.Io.Writer, path: []const u8) !bool {
|
||||
if (unsupportedPathByte(path)) |byte_name| {
|
||||
try printUnsupportedPath(io, byte_name);
|
||||
return false;
|
||||
}
|
||||
const st = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| {
|
||||
try printError(io, path, err);
|
||||
return false;
|
||||
};
|
||||
try writer.print("{s}\t{s}\t{}\t{o}\t{}\n", .{ path, kindName(st.kind), st.size, modeValue(st), st.mtime.nanoseconds });
|
||||
return true;
|
||||
}
|
||||
|
||||
fn printStdinError(io: std.Io, comptime message: []const u8) !void {
|
||||
var err_buf: [512]u8 = undefined;
|
||||
var errw = std.Io.File.stderr().writer(io, &err_buf);
|
||||
try errw.interface.writeAll("facts: stdin: " ++ message ++ "\n");
|
||||
try errw.interface.flush();
|
||||
}
|
||||
|
||||
fn printStdinLine(io: std.Io, writer: *std.Io.Writer, raw: []const u8) !bool {
|
||||
if (raw.len == 0) return true;
|
||||
const ok = try printFact(io, writer, raw);
|
||||
try writer.flush();
|
||||
return ok;
|
||||
}
|
||||
|
||||
fn readStdinLines(io: std.Io, writer: *std.Io.Writer) !bool {
|
||||
var had_error = false;
|
||||
var read_buf: [8192]u8 = undefined;
|
||||
var line_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;
|
||||
var line_len: usize = 0;
|
||||
var dropping_long_line = false;
|
||||
|
||||
while (true) {
|
||||
const n = try std.posix.read(std.posix.STDIN_FILENO, &read_buf);
|
||||
if (n == 0) break;
|
||||
for (read_buf[0..n]) |byte| {
|
||||
if (byte == '\n') {
|
||||
if (!dropping_long_line) {
|
||||
if (!try printStdinLine(io, writer, line_buf[0..line_len])) had_error = true;
|
||||
}
|
||||
line_len = 0;
|
||||
dropping_long_line = false;
|
||||
continue;
|
||||
}
|
||||
if (dropping_long_line) continue;
|
||||
if (line_len == line_buf.len) {
|
||||
try printStdinError(io, "input path exceeds max path length");
|
||||
had_error = true;
|
||||
line_len = 0;
|
||||
dropping_long_line = true;
|
||||
continue;
|
||||
}
|
||||
line_buf[line_len] = byte;
|
||||
line_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dropping_long_line and line_len != 0) {
|
||||
if (!try printStdinLine(io, writer, line_buf[0..line_len])) had_error = true;
|
||||
}
|
||||
return had_error;
|
||||
}
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
const allocator = init.gpa;
|
||||
|
||||
var help_args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
|
||||
defer help_args.deinit();
|
||||
_ = help_args.skip();
|
||||
while (help_args.next()) |arg| {
|
||||
if (isHelp(arg)) {
|
||||
try usage(std.Io.File.stdout(), init.io);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
|
||||
defer args.deinit();
|
||||
_ = args.skip();
|
||||
|
||||
var out_buf: [8192]u8 = undefined;
|
||||
var out = std.Io.File.stdout().writer(init.io, &out_buf);
|
||||
try out.interface.writeAll("path\tkind\tsize\tmode\tmtime_ns\n");
|
||||
|
||||
var saw_arg = false;
|
||||
var had_error = false;
|
||||
while (args.next()) |arg| {
|
||||
saw_arg = true;
|
||||
if (!try printFact(init.io, &out.interface, arg)) had_error = true;
|
||||
}
|
||||
if (!saw_arg) {
|
||||
try out.interface.flush();
|
||||
had_error = readStdinLines(init.io, &out.interface) catch |err| die(init.io, "facts: stdin: {s}\n", .{@errorName(err)});
|
||||
}
|
||||
try out.interface.flush();
|
||||
if (had_error) std.process.exit(1);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
tool=${1:-zig-out/bin/facts}
|
||||
work=.zig-cache/facts-smoke
|
||||
mkdir -p "$work/dir"
|
||||
printf abc > "$work/a.txt"
|
||||
|
||||
wait_for_line() {
|
||||
pattern=$1
|
||||
file=$2
|
||||
label=$3
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
if grep -q "$pattern" "$file" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
i=$((i + 1))
|
||||
sleep 0.1
|
||||
done
|
||||
echo "facts smoke: timed out waiting for $label" >&2
|
||||
if [ -n "${stream_pid:-}" ]; then
|
||||
kill "$stream_pid" 2>/dev/null || true
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
"$tool" "$work/a.txt" "$work/dir" > "$work/arg.out"
|
||||
head -n 1 "$work/arg.out" | grep -q '^path kind size mode mtime_ns$'
|
||||
grep -q "^$work/a.txt file 3 " "$work/arg.out"
|
||||
grep -q "^$work/dir dir " "$work/arg.out"
|
||||
|
||||
printf '%s\n' "$work/a.txt" | "$tool" > "$work/stdin.out"
|
||||
grep -q "^$work/a.txt file 3 " "$work/stdin.out"
|
||||
|
||||
tab_path="$work/tab name"
|
||||
printf x > "$tab_path"
|
||||
if "$tool" "$tab_path" > "$work/tab-argv.out" 2> "$work/tab-argv.err"; then
|
||||
echo 'facts: argv tab path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(wc -l < "$work/tab-argv.out" | tr -d ' ')" -eq 1
|
||||
grep -q '^path kind size mode mtime_ns$' "$work/tab-argv.out"
|
||||
grep -q 'contains tab' "$work/tab-argv.err"
|
||||
if printf '%s\n' "$tab_path" | "$tool" > "$work/tab-stdin.out" 2> "$work/tab-stdin.err"; then
|
||||
echo 'facts: stdin tab path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(wc -l < "$work/tab-stdin.out" | tr -d ' ')" -eq 1
|
||||
grep -q '^path kind size mode mtime_ns$' "$work/tab-stdin.out"
|
||||
grep -q 'contains tab' "$work/tab-stdin.err"
|
||||
|
||||
newline_path=$(printf '%s/new\nname' "$work")
|
||||
printf y > "$newline_path"
|
||||
if "$tool" "$newline_path" > "$work/newline-argv.out" 2> "$work/newline-argv.err"; then
|
||||
echo 'facts: argv newline path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(wc -l < "$work/newline-argv.out" | tr -d ' ')" -eq 1
|
||||
grep -q '^path kind size mode mtime_ns$' "$work/newline-argv.out"
|
||||
grep -q 'contains newline' "$work/newline-argv.err"
|
||||
|
||||
cr_path=$(printf '%s/cr\rname' "$work")
|
||||
printf z > "$cr_path"
|
||||
if "$tool" "$cr_path" > "$work/cr-argv.out" 2> "$work/cr-argv.err"; then
|
||||
echo 'facts: argv carriage-return path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(wc -l < "$work/cr-argv.out" | tr -d ' ')" -eq 1
|
||||
grep -q '^path kind size mode mtime_ns$' "$work/cr-argv.out"
|
||||
grep -q 'contains carriage return' "$work/cr-argv.err"
|
||||
if printf '%s\r\n' "$work/a.txt" | "$tool" > "$work/cr-stdin.out" 2> "$work/cr-stdin.err"; then
|
||||
echo 'facts: stdin carriage-return path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(wc -l < "$work/cr-stdin.out" | tr -d ' ')" -eq 1
|
||||
grep -q '^path kind size mode mtime_ns$' "$work/cr-stdin.out"
|
||||
grep -q 'contains carriage return' "$work/cr-stdin.err"
|
||||
|
||||
stream_in=$work/stream.in
|
||||
rm -f "$stream_in"
|
||||
mkfifo "$stream_in"
|
||||
stream_out=$work/stream.out
|
||||
stream_err=$work/stream.err
|
||||
: > "$stream_out"
|
||||
: > "$stream_err"
|
||||
stream_pid=
|
||||
"$tool" < "$stream_in" > "$stream_out" 2> "$stream_err" &
|
||||
stream_pid=$!
|
||||
exec 3> "$stream_in"
|
||||
wait_for_line '^path kind size mode mtime_ns$' "$stream_out" 'streaming header before stdin EOF'
|
||||
printf '%s\n' "$work/a.txt" >&3
|
||||
wait_for_line "^$work/a.txt file 3 " "$stream_out" 'first streaming record before stdin EOF'
|
||||
kill -0 "$stream_pid"
|
||||
printf '%s\n' "$work/dir" >&3
|
||||
wait_for_line "^$work/dir dir " "$stream_out" 'second streaming record before stdin EOF'
|
||||
exec 3>&-
|
||||
wait "$stream_pid"
|
||||
stream_pid=
|
||||
rm -f "$stream_in"
|
||||
if [ -s "$stream_err" ]; then
|
||||
cat "$stream_err" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$tool" --help > "$work/help.out"
|
||||
grep -q '^usage: facts \[PATH\.\.\.\]' "$work/help.out"
|
||||
! grep -q '^path kind size mode mtime_ns$' "$work/help.out"
|
||||
"$tool" -h > "$work/help-short.out"
|
||||
grep -q '^usage: facts \[PATH\.\.\.\]' "$work/help-short.out"
|
||||
! grep -q '^path kind size mode mtime_ns$' "$work/help-short.out"
|
||||
|
||||
if "$tool" "$work/a.txt" "$work/missing" > "$work/error.out" 2> "$work/error.err"; then
|
||||
echo 'facts: missing path unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q "^$work/a.txt file 3 " "$work/error.out"
|
||||
grep -q "^facts: $work/missing: " "$work/error.err"
|
||||
Reference in New Issue
Block a user