Skip to content

Repository files navigation

synthtwin

Status: early (Phase 4 closed; Phase 5 next). synthtwin is not on PyPI. What exists today is the whole workflow -- the profiler, which reads a CSV table on your computer and describes it; the generator, which builds the synthetic twin from that description and nothing else; and the validator, which measures a written file against the description and writes the quality report -- plus the security baseline the whole project rests on. Every capability on this page is tagged [built] or [planned] so there is no ambiguity about which is which.

The one thing to know before you use a twin: it is faithful one column at a time. It carries no cross-column structure at all -- see "What the twin does not carry" below, which is the section to read before any other.

This repository goes public at Phase 3's visibility flip - the owner decision recorded in the Phase 3 plan, executed the moment that plan landed on the default branch. The open-source commitment is unchanged: there is no private core, every line of the product lives here, and nothing here depends on anything a contributor cannot see. The governance controls that required a public repository - branch and tag rulesets among them - are applied at the flip, and their evidence lands in SECURITY.md's activation record; no control is claimed before its evidence exists.

synthtwin creates a synthetic twin of a tabular dataset: a table with the same shape as yours, and with each column behaving like the same column of yours, worked out from a description of your table rather than from its rows - built entirely on your machine, with no network access of any kind. What that does and does not promise about your rows is set out in "The twin's rows, stated exactly" below, and what it does and does not promise about two columns together is set out in "What the twin does not carry". Both are worth reading before you rely on a twin.

What synthtwin does today [built]

Given one table of real data, three commands produce five files, of four kinds:

  1. A synthetic twin - a table of the same shape whose columns each behave like the matching column of the original, every cell of it worked out from the description rather than taken from your file.
  2. A schema file - a plain description of every column: its type, its range or its categories, and how the twin version of it was built. You get it twice, once for a program to read and once in words.
  3. A generation report - written beside every twin, saying which of the description's facts the twin holds exactly, which it holds only approximately (with the value the twin actually reached printed beside the value the description publishes), and which it does not hold at all.
  4. A plain-language quality report - written by synthtwin validate, which measures a CSV file against the description and says which of its obligations the file meets, which it misses, and which nothing written in a CSV could evidence either way. A passing report means exactly one thing: no checkable obligation was missed. It is not a verdict that the twin is fit for an analysis, and it cannot tell a synthetic file from a real one.

What synthtwin does not do yet [planned]

  1. A relationships file - the dependencies between columns, so that a twin could preserve them. Nothing of this exists today. The description carries a reserved block for it whose every slot is empty, and synthtwin refuses a description that fills one. Until it is built, the twin carries no cross-column structure at all. Nothing the quality report checks is a cross-column fact, because the description publishes none.

Who it is for

Researchers who work with records that must never go anywhere near AI tooling, cloud services, or any network - and who are not programmers. The tool is being designed to run from a single command, and every error message is required to tell a non-programmer what happened and what to do next.

What works today

Three commands, run one after the other. Each one ends by printing the next.

synthtwin profile my-table.csv
synthtwin generate my-table-profile.json
synthtwin validate my-table-profile.json --twin my-table-twin.csv

The first reads my-table.csv on your computer and writes three files beside it:

  • my-table-profile.json - the description the twin will be built from;
  • my-table-profile.txt - the same description in plain language, which is also printed on the screen;
  • my-table-questions.json - the columns synthtwin could read more than one way, what it saw in each and what it read each as, with the answers you can give. Fill it in and hand it back with --answers. It is written on every run, whether there were questions or not.

The profiler reads what each column holds -- whole numbers, measured numbers, dates, a set of categories, two-value columns, free text -- says in words why it read it that way, reports what is missing and how the missing values were written, and tells you exactly which of your real values ended up in the profile and which did not.

There is one reading it never makes for you: record numbers. A column of ID codes and a column of measurements can look identical, and getting that wrong either publishes identifiers or destroys a distribution the twin exists to reproduce. So no rule anywhere in synthtwin can reach that reading from the values in a column -- it comes only from you, through --identifier and --code, and both are described below with the rest of the options. Where synthtwin meets a column it cannot read on its own, it asks you rather than guessing.

Read that last part before you move the profile anywhere. The profile is computed from your real data. It contains no rows of your table, and it never contains a value from a column you named with --identifier or a line of free text -- but it does contain the smallest and largest values of your numeric and date columns, the points in between that describe their shape, and, for each label, the exact spellings your file used for it together with how many rows wrote it that way. By default that includes labels only one row held. A twin is not a twin if a rare finding never reaches it, so synthtwin names every value and says how many rows shared it. What a named rare value tells anybody is that somebody in your table had it -- synthtwin publishes nothing that crosses two columns, so it says nothing about who, or about anything else that person's row holds. If your review board or a data-use agreement needs groups kept above a size, --smallest-group 11 pools everything under eleven rows and the whole workflow runs on the result. What each setting costs is written out under the options below. It is real-derived material, and your institution's rules for such material apply to it. The same is true of every other file a full run produces: the profile, the plain-language summary beside it, the questions file, the twin, the twin's report and the quality report all carry facts computed from your real data, so those rules apply to all six, not to the profile alone. The summary is on that list for the reason that makes it easy to forget -- it is the readable one, so it is the one that gets pasted into an email, and it repeats the real labels the profile publishes.

The second reads that description -- and nothing else, not your table again -- and writes two more files beside it:

  • my-table-twin.csv - the synthetic twin;
  • my-table-twin-report.txt - what the twin carries, what it only approximates and what it does not carry at all, in plain language. It is also printed on the screen. Keep it with the twin: it is the only place that says which of those three each fact is.

The twin has the same columns in the same order, the same number of rows, and the same amount of missing data as your table, and each of its columns behaves like the matching column of yours. Read "What the twin does not carry" below before you draw anything from two of its columns at once.

The third reads the description and one CSV file -- by default the twin beside it, or whatever --twin names -- and writes one more file:

  • my-table-twin-quality.txt - the quality report, which lists every obligation the description sets, the outcome of each, and every obligation no CSV can evidence either way, with the reason. It is also printed on the screen. Its name comes from the file it measured and its first lines say which file that was, so checking a second candidate writes a second report rather than overwriting the first.

The exit code carries the same answer for a script: 0 when nothing was missed, 3 when something was, 1 when the check could not run at all, and 2 when the command line could not be used. A tool reading exit codes can therefore tell a file that failed its check from a file that was never evaluated, without parsing prose.

The options

Six for profile, for the things the rules cannot settle on their own:

synthtwin profile my-table.csv --out-dir reports
synthtwin profile my-table.csv --identifier participant_number
synthtwin profile my-table.csv --code vaccine_code
synthtwin profile my-table.csv --measurement blood_pressure
synthtwin profile my-table.csv --smallest-group 11
synthtwin profile my-table.csv --keep-value -999
synthtwin profile my-table.csv --missing-value NA
synthtwin profile my-table.csv --first-row data

--out-dir writes the files into a folder you name instead of into the folder your table is in. The folder has to exist already.

--answers names the questions file from an earlier run, with your answers written in. Open my-table-questions.json, write one of the answers offered beside your_answer for the columns you want to correct, save it, and run synthtwin profile on the same table naming that file here. Your answers become the declarations -- exactly as if you had typed --code, --identifier or --measurement for each of them -- and the description records them as such. Columns you leave blank keep the reading synthtwin made. An answer the question did not offer stops the run and tells you what you can write instead, rather than being quietly dropped.

--identifier names a column whose values are record numbers or codes rather than measurements, so that none of them are published anywhere in that column's description. It takes a column name -- any column, whatever that column holds -- and it is the only way a column is ever read that way. Repeat it to name more than one column. A name that is not in your table stops the run before anything is written.

--measurement names a column that holds measurements written as two or more numbers in one cell -- a blood pressure such as 120/80, a ventilator ratio such as 1:1.5. synthtwin reads each number separately and publishes a range and an average for each one, so the twin's cells hold believable readings instead of digits in the right shape. Without it such a column is described as text and its twin carries no readings at all. Use it only where the numbers are quantities: a lab code such as 1923-1 and a drug code such as 00052-0052-52 are written exactly the same way and are codes, so name those with --code. A column of plain single numbers needs nothing.

It also settles a second shape. A column whose number sits inside an electronic address -- user12345@example.org -- is described by a later rule instead, because the average of a column of real addresses is the average of whatever numbers those addresses were given. Such a column says so in its own description, and naming it here says the number inside is a quantity after all.

The numbers move together, and you are not asked about that. How strongly a reading's two numbers rise and fall together, and whether the first is always the larger, are things your data says plainly -- so synthtwin measures them and builds the twin to match, rather than asking you. On a real blood-pressure column that is the difference between a twin of believable readings and one holding a diastolic above its systolic.

--code names a column that holds a coding system rather than measurements -- vaccine codes, procedure codes, revenue codes, provider numbers, risk-group codes. Its values are still published, because which codes are common is the point of the column; what changes is that synthtwin stops reading them as numbers, so 08 stays 08 instead of coming back as 8, and the column gets a count per code instead of an average, a smallest and a largest -- which for a code are meaningless and are real codes besides. You need it only for a column written in digits alone: one written with a letter or a dash, like E11.9 or 0002-8215-01, is already read as codes. Repeat it to name more than one column, and use --identifier instead for a record number nothing should publish.

Naming a column here also makes it publish at all. A column of many different codes, none of them repeated much -- a laboratory code, a drug code, a gene variant -- is otherwise read as free text, which publishes no value whatever. Named with --code it publishes every code with the number of rows that held it, and the twin holds the same codes in the same proportions. That is what makes counting on the twin come out right: because it holds the same codes the same number of times, every rollup of that column reproduces exactly -- the prefix a hierarchy groups by, the segment a reader splits on, the length. synthtwin knows no coding system and does not need to.

synthtwin asks you about this rather than guessing. A column of 08, 20, 213 is vaccine codes or it is counts, and the two are written identically -- nothing in the values can settle it, so synthtwin does not try. When it meets a column of digits that looks like it could be codes (some value padded with a leading zero, or every value the same width), it stops and asks you, showing you a few of the values. Your answers go into the profile, and the exact options to repeat the run without the questions are printed at the end. Where nobody is at the keyboard -- a script, a pipeline, CI -- it never stops: it names those columns on screen, says what it assumed, and prints the --code line that corrects it.

--smallest-group, and what raising it does. It changes how many rows a group needs before the profile names it. The default is 1, which holds nothing back: every value your table holds is named, together with how many rows shared it, so a rare finding reaches your twin. Any whole number of 1 or more is accepted end to end: profile, generate and validate all run on the file it produces. Raising it publishes less -- --smallest-group 11 pools every group under eleven rows, which is what a review board or a data-use agreement usually means by a small-cell rule. At the default of 1 the profile publishes small groups and their counts, and that is worth reading slowly, because the count is the disclosure rather than a route to one. At a smallest group size of two, the profile names values that two rows shared and says that two rows shared them; at one, it names a value one row held and says that one row held it. If one row of your table is one person, somebody who already knows one true thing about someone in it -- that they are in it at all -- can find the small group that person must be in and read off everything else the profile says about that group. Eleven is the number that keeps a published group too big for that.

The counts do not stop at the profile: the twin is built to hold them exactly, and the summary, the questions file, the twin's report and the quality report quote them back, so all six files of a run carry them. synthtwin does not refuse the option -- it is your table and your institution's rules -- but a run at a lowered number prints an unmissable warning before either file exists, and each of the four readable files says on its own face that it was made that way, so that a colleague handed one of them alone can tell.

--keep-value names a value your table means as real data even though synthtwin would otherwise read it as "no value" -- a region genuinely coded NA, or -999 as a real reading. --missing-value is the opposite: a value synthtwin would keep that your table means as "no value". A value that reads as a number is matched as a number, so -999 also covers -999.00; anything else is matched as text, ignoring surrounding spaces and capitals. The profile records how many values you named each way and the rule that matched them. Where the value you named is one of synthtwin's own twenty-three published words for "no value" -- the eighteen spellings such as NA, null and the spreadsheet error literals like #N/A, the three stand-in numbers -9999, -999 and 9999, and the two placeholder days 1900-01-01 and 9999-12-31 -- it also records which of those words it was, because a check of your own table against its own description has to read those cells the way the description read them. A word of your own is written nowhere in the settings, no count, column or row goes with the ones that are recorded, and SECURITY.md states the delta and its bound. That is a rule about the settings, and not about the rest of the description. A word you name with --missing-value IS written into the description: the column that counted those cells names the spelling exactly as your table wrote it, wherever at least --smallest-group rows hold it and that column publishes any values at all. So do not name a diagnosis, a code or an identifier here without deciding first that the description may carry it - a profile run that writes one of your words says so on the screen before either file exists, and the plain-language summary lists every word of yours the description names. And a value you keep is ordinary data from then on, so it can appear wherever its column publishes values, for instance as that column's smallest number.

--first-row, and the assumption it takes back. When a file settles the question, synthtwin follows the file. When nothing in the file settles it, synthtwin reads the first row as the column names, because that is how a table is normally written -- and taking that reading is not the same as proving it, so it is written down rather than assumed silently. The profile records that the names were taken by convention, and the summary says so in plain words near the top, ahead of everything the assumption would change. The cost, stated instead of hidden: if your file has no column names, its first record is described as column names and is left out of every count. --first-row data takes that back -- synthtwin then names the columns column_1, column_2, and so on, and keeps every record. --first-row names settles it the other way. When the file itself shows that the first row is a record, synthtwin stops and asks rather than choosing for you.

Three for generate:

synthtwin generate my-table-profile.json --out-dir reports
synthtwin generate my-table-profile.json --seed 7
synthtwin generate my-table-profile.json --replace

--out-dir works as it does above. --seed is a whole number from 0 to 18446744073709551615 that decides which twin you get: the same description, seed and version of synthtwin always give the same twin, byte for byte, and a different seed gives a different twin that follows the description just as closely. --replace lets a re-run write over the twin and the report an earlier run left at those names; without it a run that finds either name taken stops and changes nothing, because synthtwin has no way of telling an earlier twin of its own from a file of yours that happens to be there.

Three for validate:

synthtwin validate my-table-profile.json --twin my-table-twin.csv
synthtwin validate my-table-profile.json --out-dir reports
synthtwin validate my-table-profile.json --replace

--twin names the CSV file to measure; left out, synthtwin measures the twin beside the description. It measures whatever file you name: it has no way of telling a twin of its own from any other CSV, and the report says so rather than implying otherwise. --out-dir works as it does above, and decides only where the quality report goes -- it says nothing about where an earlier generate run put its twin, which is why the line the generator prints when it finishes always spells out --twin. --replace lets a re-run write over a quality report an earlier run left at that name, on the same reasoning as above.

What the twin does not carry

This is the section to read before you trust a number computed from a twin, and it is not a list of bugs -- it is what this version of synthtwin models.

No cross-column structure, of any kind. Every column of the twin is built on its own, from the facts the description publishes about that column alone. Nothing that links two columns of your table is in the twin: not a taller person weighing more, not a later date costing more, not a code that only ever appears beside one region, not a column that is another column times twelve, not two columns that are empty in the same rows, and not one event date always falling before another. Analysis code you develop on the twin runs, which is what the twin is for; a number that code computes from two columns of the twin means nothing about your table.

Rows are treated as independent, and the grain is undescribed. The description never says what one row of your table is. If your table holds several rows per person, per visit or per site, the twin does not know that, so anything that groups rows -- an average per person, a repeated-measures model, a count of visits each -- behaves differently on the twin than it will on your table. The twin is faithful one row at a time, and a twin of a repeated-measures table misdescribes the subject-level truth even where every column of it is right on its own.

Cross-column structure arrives in a later phase (Phase 5, in docs/plans/). Every twin's own report states both limits, on every run, whether or not anything else went wrong.

What exists today

  • [built] synthtwin profile - the reading and column analysis described above.
  • [built] synthtwin generate - the twin and the report described above, built from the description and a seed and from nothing else. The generator never opens your table; it is not given a path to one.
  • [built] synthtwin validate - the quality report described above, measured by describing the file again with the profiler's own producer and comparing. It never reaches the generator, so its verdicts are a second opinion rather than the planner marking its own work, and it consumes no randomness at all.
  • [built] The generation report, which names every published fact the twin missed, every fact it only approximates with the bound it was held to, and the two limits under "What the twin does not carry" above. It passes no verdict of its own and says so, and it ends by teaching the validate command line that produces one.
  • [built] The synthtwin command's version and status output.
  • [built] The offline guarantee's layered checks: a best-effort import-allowlist scanner for the source tree, a socket guard in the test suite, and a packaged build that runs with no network available at all. SECURITY.md states exactly what the scanner does and does not prove.
  • [built] The decontamination system: a scanner, a hashed manifest, and a signed attestation that together keep private-environment vocabulary out of this repository (see SECURITY.md).
  • [built] The data-provenance guard: no data-format file is tracked anywhere in the repository except a test fixture listed in the fixture manifest, and every such fixture must be rebuilt from its committed generating script and byte-compared in CI.
  • [built] Continuous integration with a single aggregate gate, and the written plans and their review record in docs/plans/.
  • [planned] Relationships between columns. The description describes each column on its own, and the twin therefore carries no cross-column structure at all; how columns move together arrives in a later phase (Phase 5).
  • [planned] PyPI publication - with signed, reproducible, attested releases. Phase 3 named it that phase's earliest-possible deliverable and Phase 3 closed on 2026-08-19 without it, so no phase carries it now: it is release engineering, waiting on its own checklist in docs/plans/phase-3-product.md and on the owner's go decision.

The security architecture, in plain language

Offline by construction [built]. synthtwin's own code contains nothing that opens a network connection, launches another program, calls native code, or loads code dynamically. It accepts only local file paths, and it is fully functional air-gapped. The claim is about the code this project ships and the work that code starts: it is verified by source audit plus layered automatic checks, and it is explicitly not an operating-system sandbox. Those automatic checks - the import-allowlist scan in particular - are best-effort layers rather than a proof that every call in the program lands on an exactly known target. SECURITY.md states their scope plainly and names the residual risks that stay open, among them code that a caller hands to synthtwin: that code runs in the caller's own process under the caller's own authority, and no check here governs it. If your institution requires enforcement rather than assurance, run synthtwin inside your own network-isolated environment; it will work there unchanged.

Profile and generator are separate [built]. The architecture keeps the profiler and the generator apart: the profiler runs where the real data lives and writes a profile file; the generator needs only that profile. The real data never has to move. The separation is held by the import graph rather than by anybody's care -- one module opens a CSV table, and a generate run never reaches it at any instant, from the moment the command starts. validate does reach it, and must: measuring a file means describing that file with the profiler's own producer. What validate never reaches is the generator, so its verdicts cannot inherit the planner's own defects and synthtwin's own random number generator is out of its reach. That is not the same as saying no random source is in the process, and it would be dishonest to write it that way: validate reads a file, reading a file means pandas, and pandas imports numpy, which loads numpy.random. What is enforced is that no module of synthtwin on that path imports a random source and that the run draws from none -- a trap over every source in the process, with the whole command run at it. The boundary this architecture keeps is that GENERATION reads a description and nothing else -- not that only one command opens a file.

Dependencies are governed [built]. synthtwin has exactly two direct runtime dependencies. pandas is justified in writing in docs/plans/phase-1-profiler.md and reduced by the import scanner to exactly one function of it, read_csv; numpy is justified in docs/plans/phase-2-generator.md and reduced to numpy.random.default_rng plus the single drawing call on the random stream it returns -- membership in an allowed library grants nothing on its own. numpy is used only by the generator's one random stream: the profiler, which is the half that reads your table, imports it nowhere. (It was declared directly in Phase 1 too, until review round 1 showed that its reductions made published statistics depend on the order of the rows; the profiler computes those statistics itself now, and what returned in Phase 2 is the random stream alone.) The policy distinguishes the direct dependency (declared with an honest lower bound that a CI job installs and tests) from the complete closure (every package, including build tooling and everything a dependency brings in with it, locked by hash and consumed frozen in CI and in the supported institutional install path). One consequence is stated plainly in SECURITY.md: the CSV reader synthtwin calls is itself capable of fetching a URL, and what keeps it from doing so is synthtwin's own path check, which refuses anything that is not a plain local path before any file is opened and is re-run immediately before the reader is handed that path.

The twin's rows, stated exactly

The generator is given the profile and a seed, and nothing else. It is never handed a path to your table, it does not open one, and it samples or copies no row of it. Every value in the twin is worked out from the description.

That is a statement about where the twin's values come from. It is not a promise that no row of the twin can equal a row of yours, and an earlier version of this page said otherwise, which was wrong. The profile publishes exact counts, and meeting them exactly can force a twin row to match a real one. The plainest case: a table of eleven rows with one column, whose single label is shared by all eleven rows, publishes that label with the count eleven -- so the twin writes that label in all eleven of its rows, and each of those rows is the row you have. Nothing was copied. The arithmetic left no other answer, and any tool that reproduces published counts exactly lands in the same place.

So synthtwin offers no formal privacy guarantee and claims no differential-privacy property. All six files a full run produces -- the profile, the plain-language summary beside it, the questions file, the twin, the twin's report and the quality report -- carry facts computed from your real data, and your institution's rules for real-derived material apply to all six, not to the profile alone. What synthtwin does give you is an architecture in which the real table never has to move, plus a written account, in SECURITY.md and in the run's own report, of exactly which real facts each file carries.

What that settles, and what it does not. The thing synthtwin buys you is that your rows never have to travel: you develop your analysis against a table worked out from a description, so the real table stays inside the environment that already holds it and never reaches an assistant. That is a strong claim and it is the one to make. What it does not buy is a finding about your obligations -- being synthetic is not by itself the answer to a privacy rule, to your institution's own rules, or to an approval your study needed, and the five files above are the reason. Whether an obligation is met is for the people who set it to say, and what this project gives them to decide with is the written account of exactly which real facts each file carries.

Honest limits

These are design limits, stated up front so nobody discovers them late:

Limit What it means for you
One flat table at a time synthtwin models a single table. Multi-table databases and cross-file joins are out of scope.
One column at a time The twin reproduces what the description publishes about each column on its own. It carries no cross-column structure at all -- no correlation, no formula between two columns, no shared pattern of empty cells, no ordering between two event dates. Cross-column structure arrives in a later phase (Phase 5). See "What the twin does not carry" above.
One row at a time Rows are treated as independent and the grain is undescribed: the description never says what one row of your table is, so a table with several rows per subject yields a twin that misdescribes the subject-level picture.
Only what the description publishes is reproduced A pattern the profiler does not publish is not in the twin, whether or not the profiler could in principle have seen it.
No free text Narrative or note columns are described by their length and word counts only; their values are never published, and the twin will not invent sentences. A column of CODES that reaches this role -- short cells, no spaces, marks from a small list -- also publishes the SHAPE its values were written in, so its twin's cells split and measure the way the real ones do. A shape carries no letter and no figure of any value: 4548-4 says 9999-9.
CSV only, for now The profiler reads comma-separated files saved as UTF-8 (or, as a fallback, Western European text). Spreadsheets, databases and columnar formats come later.
The table has to fit in memory A table is read into memory whole; reading very large files in pieces is planned but not built. A file of a few hundred megabytes is comfortable on an ordinary machine; several gigabytes is not, and you are told so in words rather than by a crash.
The file is read twice Once to check its shape and once to read its values, by two different readers whose results must agree. That costs a second pass over the file and buys the guarantee that a malformed row is refused rather than quietly turned into missing values.
Small tables degrade With few rows, the statistics the profiler measures are noisy, and the twin's fidelity drops accordingly. Two files say where: the generation report names each approximate fact with the value the twin reached beside the published one, and synthtwin validate writes the quality report, which is where a small table shows as missed obligations rather than as a feeling.
The quality report protects the report, not the file synthtwin validate may say about the file it checked only what synthtwin profile run on that file would publish, and prints WITHHELD where it may not say a number. That protects the report, which travels to people who may not hold the file. It is not a defence against somebody who has the checked file and runs the check again and again with descriptions they wrote themselves, watching which lines change: that person can narrow a number one report withholds, and synthtwin does not try to stop them, because they can read the file. The control that matters is who may hold the file. See SECURITY.md.

Determinism [built]

The guarantee, exactly as scoped: the same profile, the same seed, the same synthtwin version, and the same locked dependency set produce byte-identical output on the same platform. Cross-platform equality is verified empirically by golden-hash tests on every cell of the CI matrix and is reported as a tested result - it is never promised beyond the tested matrix. One documented consequence of the single-stream design: changing the schema shifts the random streams that follow it at the same seed, so byte-stability is promised only across identical inputs.

Installing

synthtwin is not on PyPI yet. To use all three commands from a clone:

git clone https://github.com/alfredocr05/synthtwin
cd synthtwin
pip install -e .
synthtwin profile my-table.csv
synthtwin generate my-table-profile.json
synthtwin validate my-table-profile.json

On a machine that must install everything by hash, and that may have no network at all, the procedure has two parts. The first needs a machine that does have network access; the second does not.

On a connected machine, collect the exact files, verifying every hash as they arrive, and copy the folder across:

pip download --require-hashes --only-binary=:all: \
    --dest wheelhouse -r requirements-install.lock

Put the synthtwin wheel (synthtwin-<version>-py3-none-any.whl, from a release) into that same folder.

On the locked-down machine, install from the folder and nothing else:

pip install --no-index --find-links wheelhouse \
    --require-hashes -r requirements-install.lock
pip install --no-index --no-deps wheelhouse/synthtwin-<version>-py3-none-any.whl

Both pip commands are barred from the network by --no-index, and the first checks every hash. --no-deps on the second is what keeps the verified versions in place.

Do not substitute pip install . from a source folder for the second command. It runs a build backend that pip fetches from the network, requirements-install.lock does not pin that backend, and on a machine with no network it simply fails. CI runs exactly the two pip commands above on every build, against the wheel produced inside a container with no network.

From Phase 3's visibility flip onward the repository is public and these commands work for anyone, with no account and no authentication; the hash-verified procedure itself is the same either way.

Running synthtwin with no arguments prints the version and what the tool can do today.

License

MIT License. Copyright (c) 2026 Alfredo Camargo Rodrigues.

This work is released on the project owner's authority as non-commercial research tooling (owner decision recorded 2026-08-07 in the Phase 0 plan). Contributions are accepted under the same license: inbound = outbound MIT, no CLA. See LICENSE for the full text.

Learn more

  • SECURITY.md - the threat model, the offline guarantee and the exact scope of its automatic checks, every named residual risk, how an auditor verifies each layer, and which governance controls are active now, with the activation record of the controls applied at the visibility flip.
  • CONTRIBUTING.md - the plan-first process and the standing rules every change must follow.
  • docs/plans/ - the written plans and their adversarial review record.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages