Skip to content

4. Project: qualify Workspace Guard

Workspace Guard snapshots a directory, verifies it later, compares manifests, and can run a child process only after verification. Its source separates commands, canonical manifests, the model, and recursive scanning.

Qualification matters more than merely building. Check and test the project, snapshot a fixed fixture tree twice, and compare bytes. Mutate one file and confirm verify fails with the documented status. Run from a path containing a space. Finally, inspect cleanup on every early return and confirm directory iteration is sorted before serialization.

This project combines owned strings and containers, borrowed views, allocation errors, multi-module imports, filesystem traversal, SHA-256, environment variables, and child processes without turning those concerns into one module.

Tested example

Qualify Workspace Guard as one package: explicit usage, root selection, exit codes, and deterministic help.

Excerpt in focus

curriculum/main-evidence/projects/workspace_guard/src/main.fl:73–116

curriculum/main-evidence/projects/workspace_guard/src/main.fl (excerpt)
error snapshot_or_verify([]const string values, bool createSnapshot) {
string root = none;
stringv manifestPath = "";
if values.len() == 4 {
error copyError;
root, copyError = model::copy_text(values[2].view());
if copyError != none {
return copyError;
}
manifestPath = values[3].view();
} else if values.len() == 3 {
bool present;
error environmentError;
root, present, environmentError = environment::get(ROOT_ENVIRONMENT);
if environmentError != none {
return environmentError;
}
if !present or root.len() == 0 {
fmt::eprint("workspace_guard: set ");
fmt::eprintln(ROOT_ENVIRONMENT);
return model::GuardError.Usage;
}
manifestPath = values[2].view();
} else {
return model::GuardError.Usage;
}
if createSnapshot {
return commands::snapshot(root.view(), manifestPath);
}
return commands::verify(root.view(), manifestPath);
}
void print_usage() {
fmt::println("workspace_guard - deterministic workspace snapshots");
fmt::println("");
fmt::println("usage:");
fmt::println(" workspace_guard snapshot <root> <manifest>");
fmt::println(" workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT");
fmt::println(" workspace_guard verify <root> <manifest>");
fmt::println(" workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT");
fmt::println(" workspace_guard diff <old-manifest> <new-manifest>");
fmt::println(" workspace_guard run <root> <manifest> -- <program> [arguments...]");
}

This excerpt depends on the other package files. The complete compiled source is available below.

Complete package source (7 files)

longbow.toml

longbow.toml
[package]
name = "workspace_guard"
version = "0.1.0"
kind = "program"
[dependencies]

longbow.lock

longbow.lock
{
"schema": 1,
"root": "root",
"packages": [
{
"id": "root",
"identity": "local/workspace_guard",
"source": {
"kind": "workspace",
"path": "."
},
"version": "0.1.0",
"dependencies": {
"std": "toolchain-std"
}
},
{
"id": "toolchain-std",
"identity": "flecha/std",
"source": {
"kind": "toolchain"
},
"dependencies": {}
}
]
}

src/commands.fl

src/commands.fl
module commands;
@imports
:: root::manifest
:: root::model
:: root::scanner
:: lib::std::containers::collections
:: lib::std::fmt
:: lib::std::process::command;
export error snapshot(stringv rootPath, stringv manifestPath) {
entries, scanError := scanner::scan(rootPath);
if scanError != none {
return scanError;
}
writeError := manifest::write(manifestPath, &entries);
if writeError != none {
return writeError;
}
fmt::print("snapshot: ");
fmt::print(entries.len());
fmt::println(" files");
return none;
}
export error verify(stringv rootPath, stringv manifestPath, bool quiet = false) {
expected, manifestError := manifest::read(manifestPath);
if manifestError != none {
return manifestError;
}
actual, scanError := scanner::scan(rootPath);
if scanError != none {
return scanError;
}
usize differences = compare(&expected, &actual, false);
if differences != 0 {
fmt::eprint("workspace_guard: ");
fmt::eprint(differences);
fmt::eprintln(" workspace differences");
return model::GuardError.Mismatch;
}
if !quiet {
fmt::print("verified: ");
fmt::print(actual.len());
fmt::println(" files");
}
return none;
}
export error diff(stringv oldManifest, stringv newManifest) {
oldEntries, oldError := manifest::read(oldManifest);
if oldError != none {
return oldError;
}
newEntries, newError := manifest::read(newManifest);
if newError != none {
return newError;
}
usize differences = compare(&oldEntries, &newEntries, true);
fmt::print("diff: ");
fmt::print(differences);
fmt::println(" differences");
return none;
}
usize compare(
&const collections::Array<model::Entry> expected,
&const collections::Array<model::Entry> actual,
bool standardOutput
) {
expectedValues := expected->view();
actualValues := actual->view();
usize expectedIndex = 0;
usize actualIndex = 0;
usize differences = 0;
while expectedIndex < expectedValues.len() or
actualIndex < actualValues.len() {
if expectedIndex == expectedValues.len() {
report("added", actualValues[actualIndex].path(), standardOutput);
actualIndex++;
differences++;
proceed;
}
if actualIndex == actualValues.len() {
report("removed", expectedValues[expectedIndex].path(), standardOutput);
expectedIndex++;
differences++;
proceed;
}
expectedPath := expectedValues[expectedIndex].path();
actualPath := actualValues[actualIndex].path();
if model::text_less(expectedPath, actualPath) {
report("removed", expectedPath, standardOutput);
expectedIndex++;
differences++;
proceed;
}
if model::text_less(actualPath, expectedPath) {
report("added", actualPath, standardOutput);
actualIndex++;
differences++;
proceed;
}
if !model::same_content(
&expectedValues[expectedIndex],
&actualValues[actualIndex]
) {
report("modified", expectedPath, standardOutput);
differences++;
}
expectedIndex++;
actualIndex++;
}
return differences;
}
void report(stringv kind, stringv path, bool standardOutput) {
if standardOutput {
fmt::print(kind);
fmt::print(" ");
fmt::println(path);
} else {
fmt::eprint(kind);
fmt::eprint(" ");
fmt::eprintln(path);
}
}
export (u32, error) run(
stringv rootPath,
stringv manifestPath,
[]const string arguments
) {
verifyError := verify(rootPath, manifestPath, true);
if verifyError != none {
return (none, verifyError);
}
if arguments.len() == 0 {
return (none, model::GuardError.Usage);
}
executable, copyError := model::copy_text(arguments[0].view());
if copyError != none {
return (none, copyError);
}
childCommand := command::Command::init(executable);
loop usize index = 1; index < arguments.len(); index++ {
argument, argumentError := model::copy_text(arguments[index].view());
if argumentError != none {
return (none, argumentError);
}
if childCommand.arg(argument) != none {
return (none, AllocationError.OutOfMemory);
}
}
child, spawnError := childCommand.spawn();
if spawnError != none {
return (none, spawnError);
}
status, waitError := child.wait();
if waitError != none {
return (none, waitError);
}
if !status.exited() {
return (none, model::GuardError.ProcessFailure);
}
return (status.detail(), none);
}

src/main.fl

src/main.fl
module main;
@imports
:: root::commands
:: root::model
:: lib::std::fmt
:: lib::std::os::arguments
:: lib::std::os::environment;
const stringv ROOT_ENVIRONMENT = "FLECHA_GUARD_ROOT";
i32 main() {
values, captureError := arguments::capture();
if captureError != none {
fmt::eprintln("workspace_guard: cannot capture command-line arguments");
return 3;
}
if values.len() < 2 {
print_usage();
return 2;
}
command := values[1].view();
error result = none;
u32 childStatus = 0;
if command == "snapshot" or command == "verify" {
result = snapshot_or_verify(values[:], command == "snapshot");
} else if command == "diff" and values.len() == 4 {
result = commands::diff(values[2].view(), values[3].view());
} else if command == "run" and values.len() >= 6 and values[4] == "--" {
childStatus, result = commands::run(
values[2].view(),
values[3].view(),
values[5:]
);
if result == none {
return childStatus.cast<i32>;
}
} else if command == "help" or command == "--help" or command == "-h" {
print_usage();
return 0;
} else {
result = model::GuardError.Usage;
}
if result == none {
return 0;
}
if result == model::GuardError.Usage {
print_usage();
return 2;
}
if result == model::GuardError.InvalidManifest {
fmt::eprintln("workspace_guard: invalid manifest");
return 4;
}
if result == model::GuardError.Mismatch {
return 5;
}
if result == model::GuardError.UnsupportedEntry {
return 6;
}
if result == model::GuardError.ProcessFailure {
fmt::eprintln("workspace_guard: child process terminated abnormally");
return 7;
}
fmt::eprintln("workspace_guard: operation failed");
return 3;
}
error snapshot_or_verify([]const string values, bool createSnapshot) {
string root = none;
stringv manifestPath = "";
if values.len() == 4 {
error copyError;
root, copyError = model::copy_text(values[2].view());
if copyError != none {
return copyError;
}
manifestPath = values[3].view();
} else if values.len() == 3 {
bool present;
error environmentError;
root, present, environmentError = environment::get(ROOT_ENVIRONMENT);
if environmentError != none {
return environmentError;
}
if !present or root.len() == 0 {
fmt::eprint("workspace_guard: set ");
fmt::eprintln(ROOT_ENVIRONMENT);
return model::GuardError.Usage;
}
manifestPath = values[2].view();
} else {
return model::GuardError.Usage;
}
if createSnapshot {
return commands::snapshot(root.view(), manifestPath);
}
return commands::verify(root.view(), manifestPath);
}
void print_usage() {
fmt::println("workspace_guard - deterministic workspace snapshots");
fmt::println("");
fmt::println("usage:");
fmt::println(" workspace_guard snapshot <root> <manifest>");
fmt::println(" workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT");
fmt::println(" workspace_guard verify <root> <manifest>");
fmt::println(" workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT");
fmt::println(" workspace_guard diff <old-manifest> <new-manifest>");
fmt::println(" workspace_guard run <root> <manifest> -- <program> [arguments...]");
}

src/manifest.fl

src/manifest.fl
module manifest;
@imports
:: root::model
:: lib::std::containers::collections
:: lib::std::encoding::hex
:: lib::std::io::buffered
:: lib::std::io::errors as io_errors
:: lib::std::io::file
:: lib::std::io::filesystem
:: lib::std::text::strings
:: lib::std::text::utf8;
const stringv HEADER = "FLECHA-WORKSPACE-SNAPSHOT\t1\n";
const usize WRITE_CAPACITY = 8192;
export error write(
stringv destination,
&const collections::Array<model::Entry> entries
) {
present, existsError := filesystem::exists(destination);
if existsError != none {
return existsError;
}
if present {
return io_errors::IoError.AlreadyExists;
}
options := build file::OpenOptions {
self.write = true;
self.createNew = true;
};
output, openError := file::File::open(destination, options);
if openError != none {
return openError;
}
error result = none;
{
adapter := output.writer();
writer, writerError :=
buffered::BufferedWriter::withCapacity(adapter, WRITE_CAPACITY);
if writerError != none {
result = writerError;
} else {
result = write_contents(&writer, entries);
}
}
if result == none {
result = output.flush();
}
closeError := output.close();
if result == none {
result = closeError;
}
if result != none {
error _ = filesystem::remove_file(destination);
}
return result;
}
error write_contents(
&buffered::BufferedWriter writer,
&const collections::Array<model::Entry> entries
) {
error writeError = writer->writeAll(HEADER.cast<[]const u8>);
if writeError != none {
return writeError;
}
values := entries->view();
loop usize index = 0; index < values.len(); index++ {
pathHex, pathError := hex::encode(values[index].path());
if pathError != none {
return pathError;
}
sizeText, sizeError :=
strings::int_to_string<u64>(values[index].byteSize());
if sizeError != none {
return sizeError;
}
writeError = writer->writeAll(
values[index].hash().cast<[]const u8>
);
if writeError == none {
writeError = writer->writeAll("\t".cast<[]const u8>);
}
if writeError == none {
writeError = writer->writeAll(sizeText.view().cast<[]const u8>);
}
if writeError == none {
writeError = writer->writeAll("\t".cast<[]const u8>);
}
if writeError == none {
writeError = writer->writeAll(pathHex.view().cast<[]const u8>);
}
if writeError == none {
writeError = writer->writeAll("\n".cast<[]const u8>);
}
if writeError != none {
return writeError;
}
}
return writer->flush();
}
export (collections::Array<model::Entry>, error) read(stringv sourcePath) {
contents, readError := filesystem::read_file(sourcePath);
if readError != none {
return (collections::Array<model::Entry>::init(), readError);
}
return parse(contents.view());
}
(collections::Array<model::Entry>, error) parse(stringv contents) {
entries := collections::Array<model::Entry>::init();
if contents.len() < HEADER.len() or
contents.substr(0, HEADER.len()) != HEADER {
return (entries, model::GuardError.InvalidManifest);
}
u32 offset = HEADER.len();
while offset < contents.len() {
u32 end = offset;
while end < contents.len() and contents[end] != '\n' {
end++;
}
if end == contents.len() or end == offset {
return (entries, model::GuardError.InvalidManifest);
}
entry, entryError := parse_line(contents.substr(offset, end - offset));
if entryError != none {
return (entries, entryError);
}
if !entries.empty() {
{
values := entries.view();
if !model::text_less(
values[values.len() - 1].path(),
entry.path()
) {
return (entries, model::GuardError.InvalidManifest);
}
}
}
pushError := entries.push(entry);
if pushError != none {
return (entries, pushError);
}
offset = end + 1;
}
return (entries, none);
}
(model::Entry, error) parse_line(stringv line) {
u32 first = line.len();
u32 second = line.len();
loop u32 index = 0; index < line.len(); index++ {
if line[index] != '\t' {
proceed;
}
if first == line.len() {
first = index;
} else if second == line.len() {
second = index;
} else {
return (none, model::GuardError.InvalidManifest);
}
}
if first == 0 or second <= first + 1 or second + 1 >= line.len() {
return (none, model::GuardError.InvalidManifest);
}
digestText := line.substr(0, first);
sizeText := line.substr(first + 1, second - first - 1);
pathText := line.substr(second + 1, line.len() - second - 1);
digestBytes, digestDecodeError := hex::decode_to_bytes(digestText);
if digestDecodeError != none or digestBytes.len() != 32 {
return (none, model::GuardError.InvalidManifest);
}
canonicalDigest, digestEncodeError := hex::encode_bytes(digestBytes[:]);
if digestEncodeError != none {
return (none, digestEncodeError);
}
if canonicalDigest.view() != digestText {
return (none, model::GuardError.InvalidManifest);
}
size, sizeParseError := strings::stringv_to_int<u64>(sizeText);
if sizeParseError != none {
return (none, model::GuardError.InvalidManifest);
}
canonicalSize, sizeFormatError := strings::int_to_string<u64>(size);
if sizeFormatError != none {
return (none, sizeFormatError);
}
if canonicalSize.view() != sizeText {
return (none, model::GuardError.InvalidManifest);
}
pathValue, pathDecodeError := hex::decode(pathText);
if pathDecodeError != none or utf8::validate(pathValue.view()) != none or
!model::valid_relative_path(pathValue.view()) {
return (none, model::GuardError.InvalidManifest);
}
canonicalPath, pathEncodeError := hex::encode(pathValue.view());
if pathEncodeError != none {
return (none, pathEncodeError);
}
if canonicalPath.view() != pathText {
return (none, model::GuardError.InvalidManifest);
}
return (model::Entry::init(pathValue, canonicalDigest, size), none);
}

src/model.fl

src/model.fl
module model;
@imports
:: lib::std::text::strings;
export error GuardError {
Usage,
InvalidManifest,
UnsupportedEntry,
Mismatch,
ProcessFailure
}
export structure Entry {
string relativePath;
string digest;
u64 size;
};
Entry Entry::init(string relativePath, string digest, u64 size) override {
return build Entry {
self.relativePath = relativePath;
self.digest = digest;
self.size = size;
};
}
stringv Entry::path(&const self) <-self {
return self->relativePath.view();
}
stringv Entry::hash(&const self) <-self {
return self->digest.view();
}
u64 Entry::byteSize(&const self) {
return self->size;
}
export bool text_less(stringv left, stringv right) {
u32 common = left.len();
if right.len() < common {
common = right.len();
}
loop u32 index = 0; index < common; index++ {
if left[index] < right[index] {
return true;
}
if left[index] > right[index] {
return false;
}
}
return left.len() < right.len();
}
export bool entry_less(&const Entry left, &const Entry right) {
return text_less(left->path(), right->path());
}
export bool same_content(&const Entry left, &const Entry right) {
return left->byteSize() == right->byteSize() and
left->hash() == right->hash();
}
export (string, error) copy_text(stringv value) {
return strings::string_from_bytes(value.cast<[]const u8>);
}
export (string, error) child_path(stringv parent, stringv name) {
if parent.len() == 0 {
return copy_text(name);
}
usize capacity = parent.len().cast<usize> + name.len().cast<usize> + 1;
if capacity <= parent.len().cast<usize> {
return (none, AllocationError.OutOfMemory);
}
builder, allocationError := strings::StringBuilder::withCapacity(capacity);
if allocationError != none {
return (none, allocationError);
}
if builder.append(parent) != none or builder.appendChar('/') != none or
builder.append(name) != none {
return (none, AllocationError.OutOfMemory);
}
return builder.toString();
}
export bool valid_relative_path(stringv value) {
if value.len() == 0 or value[0] == '/' or
value[value.len() - 1] == '/' {
return false;
}
u32 segmentStart = 0;
loop u32 index = 0; index <= value.len(); index++ {
if index < value.len() {
const char BYTE = value[index];
const u8 BYTE_VALUE = BYTE.cast<u8>;
if BYTE_VALUE < 32 or BYTE_VALUE == 127 or BYTE == '\\' or
BYTE == ':' or BYTE == '*' or BYTE == '?' or BYTE == '"' or
BYTE == '<' or BYTE == '>' or BYTE == '|' {
return false;
}
if BYTE != '/' {
proceed;
}
}
const u32 LENGTH = index - segmentStart;
if LENGTH == 0 {
return false;
}
segment := value.substr(segmentStart, LENGTH);
if segment == "." or segment == ".." {
return false;
}
segmentStart = index + 1;
}
return true;
}

src/scanner.fl

src/scanner.fl
module scanner;
@imports
:: root::model
:: lib::std::containers::collections
:: lib::std::cryptography::sha2
:: lib::std::encoding::hex
:: lib::std::fmt
:: lib::std::io::buffered
:: lib::std::io::file
:: lib::std::io::path
:: lib::std::text::utf8;
const usize READ_CAPACITY = 32768;
export (collections::Array<model::Entry>, error) scan(stringv rootPath) {
entries := collections::Array<model::Entry>::init();
scanError := collect(rootPath, "", &entries);
if scanError != none {
return (entries, scanError);
}
entries.sortBy(model::entry_less);
return (entries, none);
}
error collect(
stringv rootPath,
stringv relativePath,
&collections::Array<model::Entry> entries
) {
string directoryPath = none;
if relativePath.len() == 0 {
error copyError;
directoryPath, copyError = model::copy_text(rootPath);
if copyError != none {
return copyError;
}
} else {
error joinError;
directoryPath, joinError = path::join(rootPath, relativePath);
if joinError != none {
return joinError;
}
}
directory, openError := file::Directory::open(directoryPath.view());
if openError != none {
fmt::eprint("workspace_guard: cannot open directory: ");
fmt::eprintln(directoryPath.view());
return openError;
}
while true {
entry, present, readError := directory.next();
if readError != none {
return readError;
}
if !present {
break;
}
if utf8::validate(entry.name()) != none {
fmt::eprintln("workspace_guard: non-UTF-8 path is unsupported");
return model::GuardError.UnsupportedEntry;
}
childRelative, relativeError :=
model::child_path(relativePath, entry.name());
if relativeError != none {
return relativeError;
}
if !model::valid_relative_path(childRelative.view()) {
fmt::eprint("workspace_guard: non-portable path: ");
fmt::eprintln(childRelative.view());
return model::GuardError.UnsupportedEntry;
}
if entry.kind() == file::EntryType.Directory {
recurseError := collect(rootPath, childRelative.view(), entries);
if recurseError != none {
return recurseError;
}
proceed;
}
if entry.kind() != file::EntryType.File {
fmt::eprint("workspace_guard: unsupported entry: ");
fmt::eprintln(childRelative.view());
return model::GuardError.UnsupportedEntry;
}
actualPath, joinError := path::join(rootPath, childRelative.view());
if joinError != none {
return joinError;
}
digest, size, hashError := hash_file(actualPath.view());
if hashError != none {
fmt::eprint("workspace_guard: cannot hash file: ");
fmt::eprintln(childRelative.view());
return hashError;
}
pushError := entries->push(
model::Entry::init(childRelative, digest, size)
);
if pushError != none {
return pushError;
}
}
return directory.close();
}
(string, u64, error) hash_file(stringv filePath) {
input, openError := file::File::open(filePath, file::OpenOptions::readOnly());
if openError != none {
return (none, none, openError);
}
hasher := sha2::Sha256::init();
u64 total = 0;
error result = none;
{
adapter := input.reader();
reader, readerError :=
buffered::BufferedReader::withCapacity(adapter, READ_CAPACITY);
if readerError != none {
return (none, none, readerError);
}
[32768]u8 chunk = none;
while true {
count, readError := reader.read(chunk[:]);
if count > 0 {
const u64 NEXT = total + count.cast<u64>;
if NEXT < total {
return (none, none, AllocationError.OutOfMemory);
}
total = NEXT;
result = hasher.update(chunk[:count]);
if result != none {
return (none, none, result);
}
}
if readError != none {
return (none, none, readError);
}
if count == 0 {
break;
}
}
}
closeError := input.close();
if closeError != none {
return (none, none, closeError);
}
digest, digestError := hasher.finalize();
if digestError != none {
return (none, none, digestError);
}
encoded, encodeError := hex::encode_bytes(digest.bytes());
return (encoded, total, encodeError);
}

The selected branch coordinates the already-separated scanner, manifest, and process modules. The complete package is required because its behavior is the interaction among those modules, not the excerpt alone.

Commands

Commands
longbow check curriculum/main-evidence/projects/workspace_guard
longbow test curriculum/main-evidence/projects/workspace_guard
longbow build curriculum/main-evidence/projects/workspace_guard
longbow run curriculum/main-evidence/projects/workspace_guard -- help

Expected output

Expected output
workspace_guard - deterministic workspace snapshots
usage:
workspace_guard snapshot <root> <manifest>
workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT
workspace_guard verify <root> <manifest>
workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT
workspace_guard diff <old-manifest> <new-manifest>
workspace_guard run <root> <manifest> -- <program> [arguments...]
exit status: 0

Related reference: /examples/workspace_guard/src/main/.

Continue to Advanced: ownership contracts and deterministic builds, or inspect the Workspace Guard command implementation.