Compare commits

..

3 Commits

Author SHA1 Message Date
slhx agent 9f7aa5aaef chore: untrack generated build cache 2026-07-20 16:45:18 +02:00
slhx agent 8c3a10e35e chore: remove generated migration artifacts 2026-07-20 16:44:53 +02:00
slhx agent 03dbc66c0c chore: point to toolset migration 2026-07-20 16:44:40 +02:00
7 changed files with 12 additions and 374 deletions
+2 -8
View File
@@ -1,8 +1,2 @@
# Zig build artifacts
.zig-cache/
zig-out/
# Local editor/OS noise
*.swp
*~
.DS_Store
/.zig-cache/
/zig-out/
-40
View File
@@ -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>`.
+10
View File
@@ -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"
```
-15
View File
@@ -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.
-16
View File
@@ -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);
}
-178
View File
@@ -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);
}
-117
View File
@@ -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"