Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
55f49ea
chore: factory block for softfac
rawphp Oct 15, 2026
e4bcce9
Merge pull request #29 from original-solutions/factory/dw-config
tomkaczocha Oct 15, 2026
7abf1e1
test: factory layer logs factory started
rawphp Oct 15, 2026
f5af01a
feat(server): wire an empty FactoryLayer through server.ts
rawphp Oct 15, 2026
595cc20
test: check-seams.sh passes allowed changes and names unlisted ones
rawphp Oct 15, 2026
f07d8e2
feat(factory): add UPSTREAM, SEAMS and check-seams.sh
rawphp Oct 15, 2026
95afcec
test(factory): boundary test for factory imports
rawphp Oct 15, 2026
611af0d
docs(factory): design copy and README
rawphp Oct 15, 2026
cc1c0ac
test(factory): import vitest helpers from vite-plus/test
rawphp Oct 15, 2026
cbcf91a
test(factory): rename into an allowed path is named, seam assertions …
rawphp Oct 15, 2026
0aadcf1
fix(factory): check-seams.sh diffs without rename detection
rawphp Oct 15, 2026
71ba6b4
fix(factory): close boundary and seam-check gaps from review
rawphp Oct 15, 2026
f1dba9a
Merge pull request #30 from original-solutions/t3/t3code-factory-issue-1
tomkaczocha Oct 15, 2026
4a84519
test(factory): check.sh runs the gate steps in order and stops at the…
rawphp Oct 15, 2026
5efa406
feat(factory): check.sh runs seams, factory tests and typechecks in o…
rawphp Oct 15, 2026
aef688a
ci(factory): factory-ci workflow runs check.sh on pull requests and p…
rawphp Oct 15, 2026
bf609da
docs(factory): gate and CI run factory/scripts/check.sh
rawphp Oct 15, 2026
30845a3
test(factory): check.sh test names its logs with a counter, not Math.…
rawphp Oct 15, 2026
c97ed4f
test(factory): check.sh test runs from a subdirectory and names calls…
rawphp Oct 15, 2026
8b45673
test(factory): pin run() env allowlist, process-group kill and output…
rawphp Oct 15, 2026
5a1d95e
feat(factory): run commands with a cleared env, a time limit and an o…
rawphp Oct 15, 2026
b60ba30
fix(factory): process tests read time through Effect's Clock
rawphp Oct 15, 2026
f49f63b
docs(factory): answer open question 2, process.ts uses node:child_pro…
rawphp Oct 15, 2026
afde3af
test(factory): ledger run id, issue key, next attempt and timestamp t…
rawphp Oct 15, 2026
b7e3a11
feat(factory): ledger run ids, issue keys and timestamps
rawphp Oct 15, 2026
208f2f9
test(factory): ledger write, read, finish and crash-rule tests
rawphp Oct 15, 2026
f2e3f81
test(factory): sessions and paths adapter cases
rawphp Oct 15, 2026
ed64725
test(factory): a TERM-trapping grandchild gets its grace before SIGKILL
rawphp Oct 15, 2026
9463426
test(factory): port lifecycle and result-line tests from softfac
rawphp Oct 15, 2026
f0a605d
feat(factory): lifecycle transitions and result-line parsing in the p…
rawphp Oct 15, 2026
a5b811b
feat(factory): run ledger writer and reader
rawphp Oct 15, 2026
9d2568d
test(factory): machine log writer and scanner tests
rawphp Oct 15, 2026
ace3154
feat(factory): machine log writer and scanner
rawphp Oct 15, 2026
0768fc4
test(factory): pin the Unicode whitespace rules of the result line
rawphp Oct 15, 2026
cbfc11c
fix(factory): give the whole group its SIGTERM grace, signal the lead…
rawphp Oct 15, 2026
2ff87a2
test(factory): text before FACTORY-DONE on the last line is no result…
rawphp Oct 15, 2026
4e286e0
test(factory): gate attempts count only a gate_command that fully parses
rawphp Oct 15, 2026
dd6c446
test(factory): pin env override, post-exit stray cleanup and argv std…
rawphp Oct 15, 2026
75d81f6
fix(factory): count a gate command only when every field parses
rawphp Oct 15, 2026
d7462b8
test(factory): machine log writes an issue key as softfac's IssueKey …
rawphp Oct 15, 2026
7d855fa
fix(factory): write a machine-log issue key as softfac's IssueKey does
rawphp Oct 15, 2026
ca6554a
fix(factory): restore the LedgerOptions type dropped by the gate-atte…
rawphp Oct 15, 2026
fdf45a1
test(factory): open never creates a run file
rawphp Oct 15, 2026
8f5aa55
fix(factory): Ledger.open opens without creating the file
rawphp Oct 15, 2026
d5845d4
test(factory): the write path cleans a hand-built cause
rawphp Oct 15, 2026
3a030ce
fix(factory): clean cause text on the write path
rawphp Oct 15, 2026
3903c62
refactor(factory): one open-and-repair helper for the run file and ma…
rawphp Oct 15, 2026
92a0df4
fix(factory): write the u64 bound as 2 ** 64 for the gate-attempt check
rawphp Oct 15, 2026
380c5ff
refactor(factory): check the event field tables cover every data key
rawphp Oct 15, 2026
53d91d5
feat(factory): sessions adapter over T3's launch and projection services
rawphp Oct 15, 2026
aa0abf2
test(factory): port config parsing tests and the config-driven start
rawphp Oct 15, 2026
45a137e
feat(factory): load the machine config and start only when it is valid
rawphp Oct 15, 2026
55551d4
test(factory): blank ids fail typed, last assistant message is the last
rawphp Oct 15, 2026
24a0161
fix(factory): blank ids fail as the declared sessions errors
rawphp Oct 15, 2026
7265e29
refactor(factory): define the config error shape once
rawphp Oct 15, 2026
b679ddd
Merge pull request #31 from original-solutions/t3/factory-issue-2
tomkaczocha Oct 15, 2026
00bade3
fix(factory): trim the result line in linear time and drop unused mar…
rawphp Oct 15, 2026
4d016bb
fix(factory): typed sync spawn errors, bounded error attributes, inte…
rawphp Oct 15, 2026
a282f3b
fix(factory): status reads only runs and assistant messages
rawphp Oct 15, 2026
a50605f
Merge pull request #36 from original-solutions/t3/factory-issue-5
tomkaczocha Oct 15, 2026
eada7ef
fix(factory): ledger handles see each other's lines, and retries sync…
rawphp Oct 15, 2026
434d8f9
Merge origin/main into t3/factory-issue-11
rawphp Oct 15, 2026
a4cdc63
Merge pull request #37 from original-solutions/t3/factory-issue-11
tomkaczocha Oct 15, 2026
20cbe28
fix(factory): an append ends a cut-off last line before checking for …
rawphp Oct 15, 2026
e88277b
refactor(factory): expose run through a FactoryProcess service
rawphp Oct 15, 2026
36b6789
fix(factory): LedgerError is a tagged error, and the line-break repai…
rawphp Oct 15, 2026
8a65faf
Merge pull request #33 from original-solutions/t3/factory-issue-8
tomkaczocha Oct 15, 2026
8420dce
test(factory): an open whose cut-off-line repair fails is Io and a la…
rawphp Oct 15, 2026
57c4bb7
fix(factory): transition returns InvalidTransition as a typed failure
rawphp Oct 15, 2026
71c5aeb
refactor(factory): one Schema.TaggedError class per ledger failure
rawphp Oct 15, 2026
783be3f
fix(factory): an answered needs-info issue can go back to ready
rawphp Oct 15, 2026
fca723a
Merge pull request #35 from original-solutions/t3/factory-issue-9
tomkaczocha Oct 15, 2026
0362d86
test(factory): separate stderr and truncation flag for FactoryProcess
rawphp Oct 15, 2026
cb35ead
Merge pull request #32 from original-solutions/t3/factory-issue-10
tomkaczocha Oct 15, 2026
2646c2d
feat(factory): separate stderr and truncation flag for FactoryProcess
rawphp Oct 15, 2026
64180c8
test(factory): port softfac's gh tracker cases
rawphp Oct 15, 2026
6ce1de0
feat(factory): gh tracker over FactoryProcess
rawphp Oct 15, 2026
c9ce5c1
Merge remote-tracking branch 'origin/main' into t3/factory-issue-12
rawphp Oct 15, 2026
3fd0495
test(factory): github tracker speaks the lifecycle State, refuses NUL…
rawphp Oct 15, 2026
353be93
fix(factory): github tracker uses the lifecycle State and refuses NUL…
rawphp Oct 15, 2026
4d396f5
test(factory): fileAt refuses an oversize file whose content is whole
rawphp Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dw/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# softfac (the Rust factory) reads this block from the default branch on every pass.
# Design and plan: factory/docs/design.md once issue #1 lands; issues #1 to #21.
factory:
instructors: [tomkaczocha, rawphp]
base: main
provision: [vp i]
gate: [factory/scripts/check.sh]
limits: { gate_retries: 2 }
autonomy: pr
44 changes: 44 additions & 0 deletions .github/workflows/factory-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Factory CI

on:
pull_request:
branches:
- main
push:
branches:
- main

permissions:
contents: read

concurrency:
group: factory-ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# This is the fork's own workflow. Upstream's workflows are disabled on this repo.
jobs:
check:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Install
run: vp i

- name: Check
run: factory/scripts/check.sh
157 changes: 157 additions & 0 deletions apps/server/src/factory/boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";

import { describe, expect, it } from "vite-plus/test";

interface SourceFile {
/** Relative to apps/server/src, with `/` separators. */
readonly path: string;
readonly imports: ReadonlyArray<string>;
}

const resolveRelative = (file: string, specifier: string) =>
NodePath.posix.normalize(NodePath.posix.join(NodePath.posix.dirname(file), specifier));

const packageName = (specifier: string) =>
specifier
.split("/")
.slice(0, specifier.startsWith("@") ? 2 : 1)
.join("/");

/**
* Factory code (except factory/t3/) may not import T3 code: neither relative paths outside
* factory/ nor the server's workspace packages. Nothing outside factory/ may import it,
* except server.ts.
*/
export const findBoundaryViolations = (
files: ReadonlyArray<SourceFile>,
workspacePackages: ReadonlySet<string>,
): Array<string> => {
const violations: Array<string> = [];
for (const file of files) {
const relative = file.imports.filter((specifier) => specifier.startsWith("."));
if (file.path.startsWith("factory/")) {
if (file.path.startsWith("factory/t3/")) continue;
for (const specifier of relative) {
if (!resolveRelative(file.path, specifier).startsWith("factory/")) {
violations.push(`${file.path} imports ${specifier} from outside factory/`);
}
}
for (const specifier of file.imports) {
if (workspacePackages.has(packageName(specifier))) {
violations.push(`${file.path} imports ${specifier}, a T3 package`);
}
}
} else if (file.path !== "server.ts") {
for (const specifier of relative) {
if (resolveRelative(file.path, specifier).startsWith("factory/")) {
violations.push(`${file.path} imports ${specifier} from factory/`);
}
}
}
}
return violations;
};

const IMPORT_PATTERNS = [
/\bfrom\s*"([^"]+)"/g,
/\bfrom\s*'([^']+)'/g,
/\bimport\s*"([^"]+)"/g,
/\bimport\s*'([^']+)'/g,
/\bimport\(\s*"([^"]+)"\s*\)/g,
/\bimport\(\s*'([^']+)'\s*\)/g,
];

const extractImports = (source: string): Array<string> =>
IMPORT_PATTERNS.flatMap((pattern) => Array.from(source.matchAll(pattern), (match) => match[1]!));

const srcRoot = NodePath.resolve(import.meta.dirname, "..");

/** The server's `workspace:` dependencies, which are T3 code. */
const readWorkspacePackages = (): ReadonlySet<string> => {
const manifest = JSON.parse(
NodeFS.readFileSync(NodePath.resolve(srcRoot, "../package.json"), "utf8"),
) as Record<string, Record<string, string> | undefined>;
return new Set(
[manifest.dependencies, manifest.devDependencies].flatMap((deps) =>
Object.entries(deps ?? {})
.filter(([, version]) => version.startsWith("workspace:"))
.map(([name]) => name),
),
);
};

const listSourceFiles = (dir: string): Array<string> =>
NodeFS.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
if (entry.name === "node_modules") return [];
const full = NodePath.join(dir, entry.name);
if (entry.isDirectory()) return listSourceFiles(full);
return /\.tsx?$/.test(entry.name) ? [full] : [];
});

describe("factory boundary", () => {
it("holds for the real tree", () => {
const files = listSourceFiles(srcRoot).map((full) => ({
path: NodePath.relative(srcRoot, full).split(NodePath.sep).join("/"),
imports: extractImports(NodeFS.readFileSync(full, "utf8")),
}));
expect(files.some((file) => file.path === "factory/index.ts")).toBe(true);
expect(files.find((file) => file.path === "server.ts")?.imports).toContain(
"./factory/index.ts",
);
const workspacePackages = readWorkspacePackages();
expect(workspacePackages.has("@t3tools/contracts")).toBe(true);
expect(findBoundaryViolations(files, workspacePackages)).toEqual([]);
});

it("flags factory code importing from outside factory/, except under factory/t3/", () => {
const violations = findBoundaryViolations(
[
{ path: "factory/foo.ts", imports: ["../config.ts", "effect/Effect", "./bar.ts"] },
{ path: "factory/t3/paths.ts", imports: ["../../config.ts"] },
],
new Set(),
);
expect(violations).toHaveLength(1);
expect(violations[0]).toContain("factory/foo.ts");
expect(violations[0]).toContain("../config.ts");
});

it("flags factory code importing T3 workspace packages, except under factory/t3/", () => {
const violations = findBoundaryViolations(
[
{
path: "factory/domain/lifecycle.ts",
imports: ["@t3tools/contracts", "@t3tools/shared/git", "effect/Effect", "node:fs"],
},
{ path: "factory/t3/threads.ts", imports: ["@t3tools/contracts"] },
],
new Set(["@t3tools/contracts", "@t3tools/shared"]),
);
expect(violations).toEqual([
"factory/domain/lifecycle.ts imports @t3tools/contracts, a T3 package",
"factory/domain/lifecycle.ts imports @t3tools/shared/git, a T3 package",
]);
});

it("flags code outside factory/ importing the factory, except server.ts", () => {
const violations = findBoundaryViolations(
[
{ path: "orchestration-v2/x.ts", imports: ["../factory/index.ts"] },
{ path: "server.ts", imports: ["./factory/index.ts"] },
],
new Set(),
);
expect(violations).toHaveLength(1);
expect(violations[0]).toContain("orchestration-v2/x.ts");
});

it("extracts static, side-effect and dynamic imports", () => {
expect(
extractImports(
`import a from "./a.ts";\nimport 'b';\nexport * from './c.ts';\nconst d = await import("./d.ts");`,
).toSorted(),
).toEqual(["./a.ts", "./c.ts", "./d.ts", "b"]);
});
});
108 changes: 108 additions & 0 deletions apps/server/src/factory/check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";

const realScript = NodePath.resolve(import.meta.dirname, "../../../../factory/scripts/check.sh");

const SEAMS = "seams";
const TEST = "vp test run apps/server/src/factory";
const SERVER_TYPECHECK = "vp run --filter t3 typecheck";
const WEB_TYPECHECK = "vp run --filter @t3tools/web typecheck";

// Both stubs append their invocation to $CHECK_TEST_LOG, then exit with the code
// from $CHECK_TEST_FAIL ("<invocation>=<code>") when it names this invocation.
const stub = (invocation: string) => `#!/usr/bin/env bash
invocation=${invocation}
echo "$invocation" >> "$CHECK_TEST_LOG"
case "\${CHECK_TEST_FAIL:-}" in
"$invocation="*) exit "\${CHECK_TEST_FAIL##*=}" ;;
esac
exit 0
`;

let root: string;
let script: string;

const write = (file: string, content: string) => {
const full = NodePath.join(root, file);
NodeFS.mkdirSync(NodePath.dirname(full), { recursive: true });
NodeFS.writeFileSync(full, content, { mode: 0o755 });
};

let runs = 0;

const run = (fail?: string, cwd = root) => {
const log = NodePath.join(root, `log-${runs++}`);
const result = NodeChildProcess.spawnSync("bash", [script], {
cwd,
encoding: "utf8",
env: {
...process.env,
PATH: `${NodePath.join(root, "bin")}:${process.env.PATH}`,
CHECK_TEST_LOG: log,
CHECK_TEST_FAIL: fail ?? "",
},
});
const calls = NodeFS.existsSync(log) ? NodeFS.readFileSync(log, "utf8").trim().split("\n") : [];
return { status: result.status, stdout: result.stdout, calls };
};

describe("check.sh", () => {
beforeAll(() => {
root = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "check-"));
script = NodePath.join(root, "factory/scripts/check.sh");
write("factory/scripts/check.sh", NodeFS.readFileSync(realScript, "utf8"));
write("factory/scripts/check-seams.sh", stub(SEAMS));
write(
"bin/vp",
`#!/usr/bin/env bash
invocation="vp $*"
echo "$invocation" >> "$CHECK_TEST_LOG"
case "\${CHECK_TEST_FAIL:-}" in
"$invocation="*) exit "\${CHECK_TEST_FAIL##*=}" ;;
esac
exit 0
`,
);
});

afterAll(() => {
NodeFS.rmSync(root, { recursive: true, force: true });
});

it("runs every step in order and exits 0", () => {
const { status, stdout, calls } = run();
expect(status).toBe(0);
expect(calls).toEqual([SEAMS, TEST, SERVER_TYPECHECK, WEB_TYPECHECK]);
const at = ["factory/scripts/check-seams.sh", TEST, SERVER_TYPECHECK, WEB_TYPECHECK].map(
(name) => stdout.indexOf(name),
);
expect(at[0]).toBeGreaterThanOrEqual(0);
expect(at).toEqual([...at].sort((a, b) => a - b));
expect(new Set(at).size).toBe(4);
});

it("runs from a subdirectory of the repo", () => {
const { status, calls } = run(undefined, NodePath.join(root, "bin"));
expect(status).toBe(0);
expect(calls).toEqual([SEAMS, TEST, SERVER_TYPECHECK, WEB_TYPECHECK]);
});

it("stops at the first failure with that command's exit code", () => {
const { status, stdout, calls } = run(`${SERVER_TYPECHECK}=3`);
expect(status).toBe(3);
expect(calls).toEqual([SEAMS, TEST, SERVER_TYPECHECK]);
expect(stdout).toContain(SERVER_TYPECHECK);
expect(stdout).not.toContain(WEB_TYPECHECK);
});

it("a failing seam check stops before any vp command", () => {
const { status, calls } = run(`${SEAMS}=2`);
expect(status).toBe(2);
expect(calls).toEqual([SEAMS]);
});
});
Loading
Loading