Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ API between minor releases.

### Added

- CLI and diagnostics: `--strict` exits with code 6 (`RvtExitCodes.PartialRead`) instead of 0 when a read finishes partial without exceeding a reader limit (a stream member failed to decode, was rebuilt by structural recovery, or decoded without a matching checksum, or `Global/ElemTable` decoded no row). Nothing is exported when exit code 6 is returned; `limits.strict` records the option and `issues[]` lists the affected streams and reasons (`failed-members`, `recovered-members`, `unverified-members`, `no-rows`) alongside `streams[]`. Default behavior without `--strict` is unchanged. (Closes #54)
- Default output, current IFC profile: every beam with a straight drive curve gets `Pset_BeamCommon.Slope`, the angle in degrees of its drive curve to the horizontal (0 for a horizontal beam, never negative), whether or not BaseQuantities are exported and whatever its body; its quantities are unchanged. A beam without a straight drive curve gets no Slope.
- Default output, current IFC profile: `Pset_BeamCommon.Span` of a beam without a stored length is the length of its straight drive curve; a beam without one has no Span.
- Default output, current IFC profile: a round column gets the volume and surface of the measured node that carries its two exact end faces in place of the open tessellated side. A round column is a vertical cylinder: exactly two exact planar end faces, horizontal, without holes, of at least eight corners on one circle (the same for both faces), each with an exact area within 0.5% of the area of that circle (a regular polygon of 32 corners or fewer is refused), and every other face with its corners on the cylinder between the end faces. The node is taken only when it is the one node that holds nothing but end faces. It is used for `NetVolume` after the sources that come first for a column (the Medium solids, the nominal area of a tube, the computed host volume, the measures of all nodes) and for `CrossSectionArea` (half the surface) of a column without extrusion data. When the two end faces also have the same exact area (a vertical prism) the column gets the extrusion data `Length`, `CrossSectionArea` (the area of its circle), `OuterSurfaceArea` and `GrossVolume`. Other columns are unchanged. Measured on the round columns of one sample (corner distances to a millionth of the radius, or eight units in the last place of a 32-bit coordinate where that is more).
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ Use `--ifc-revit-parameter-groups` for one property set per parameter group. A f
| 3 | A stream exceeded a limit of the reader and the read was cut. Nothing is exported unless `--allow-partial` is given. |
| 4 | The file is readable but a decoder failed on its content. |
| 5 | `downgrade --check` refused the request, for example because the source is newer than release 2026, the profile is not older than the source, the saving release of a file does not match its class table, or a class name occurs twice in a table. One line on standard error. |
| 6 | `--strict` was given and the read was partial without hitting a limit (a member failed to decode, was rebuilt by structural recovery, or decoded without a matching checksum, or the element table decoded no row). Nothing is exported; the report lists the streams and the reason. |

`--report project.report.json` writes a machine-readable report: the fraction of each partition stream and of the element table that decoded, the streams cut by a limit, the elements the export left out with their class and reason, and the exported fraction. `--raise-limits` raises the limits for a very large model. A complete export without these options behaves as before. The report format and the limits are described in [the export report](docs/export-report.md).
`--report project.report.json` writes a machine-readable report: the fraction of each partition stream and of the element table that decoded, the streams cut by a limit, the elements the export left out with their class and reason, and the exported fraction. `--strict` makes partial reads that did not hit a limit fail with exit code 6 instead of exporting with exit code 0. `--raise-limits` raises the limits for a very large model. A complete export without these options behaves as before. The report format and the limits are described in [the export report](docs/export-report.md).

### Plan a downgrade to an older release (experimental)

Expand Down
19 changes: 11 additions & 8 deletions docs/export-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ Without the new options the output (IFC, JSON, standard error text) and the exit
| Option | Effect |
| --- | --- |
| `--report <report.json>` | Write the JSON report of the run to this path. Written on success and on failure. |
| `--allow-partial` | Export what was read when a reader limit cut a stream. The exit code is then 0 and the report says `"status": "partial"`. |
| `--allow-partial` | Export what was read when a reader limit cut a stream. The exit code is then 0 and the report says `"status": "partial"`. Cannot be combined with `--strict`. |
| `--strict` | Fail with exit code 6 instead of 0 when the read is partial even without hitting a limit (a member failed to decode, was rebuilt by structural recovery, or decoded without a matching checksum, or the element table decoded no row). Nothing is exported; the report lists the streams and the reason. Cannot be combined with `--allow-partial`. |
| `--raise-limits` | Raise the limits listed below for a very large model. |

## Exit codes

| Code | Name | Meaning |
| --- | --- | --- |
| 0 | success | The export was written. With `--allow-partial` or after members of a stream failed to decode, the report can still say `"status": "partial"`: read it when you need to know. |
| 0 | success | The export was written. With `--allow-partial` or, without `--strict`, after members of a stream failed to decode or were recovered, the report can still say `"status": "partial"`: read it when you need to know, or pass `--strict` to make partial reads fail. |
| 1 | usage | The command line is wrong (unchanged from earlier versions). |
| 2 | input error | The input cannot be read: the file does not exist or cannot be opened, is empty, is not a compound file, or is cut or damaged so that a stream ends before its declared length. One line on standard error, no stack trace, nothing exported. |
| 3 | limit exceeded | A stream exceeded a limit of the reader and the read was cut. Nothing is exported unless `--allow-partial` is given. A stream longer than the platform allows in one array cannot be read at all: that case exits with 3 even with `--allow-partial`. |
| 4 | decode error | The compound file is readable but a decoder failed on its content (damaged or unsupported data). One line on standard error; the report holds the exception with its stack trace. |
| 5 | refused | `downgrade --check` refused the request (for example a source newer than release 2026, a profile not older than the source, a saving release that does not match the class table, or an ambiguous class name). One line on standard error. |
| 6 | partial read | `--strict` was given and the read finished partial without exceeding a limit (members of a stream failed to decode, were rebuilt by structural recovery, or decoded without a matching checksum, or the element table decoded no row). One line on standard error naming the streams and the reason, nothing exported; the report holds the stream counts and `issues[]`. |

A crash that is none of these (a defect in the exporter after the read) still ends the process with the runtime's unhandled exception code.

Expand All @@ -44,7 +46,7 @@ The report is one JSON object, UTF-8, property names in camelCase. `schema` name
"status": "complete",
"exitCode": 0,
"input": { "file": "project.rvt", "bytes": 9293824 },
"limits": { "raised": false, "partialAllowed": false, "hit": [] },
"limits": { "raised": false, "partialAllowed": false, "strict": false, "hit": [] },
"streams": [
{ "name": "Partitions/0", "rawBytes": 8187086, "members": 695, "decoded": 695,
"failed": 0, "recovered": 0, "unverified": 0, "decodedFraction": 1 }
Expand All @@ -62,15 +64,15 @@ The report is one JSON object, UTF-8, property names in camelCase. `schema` name

| Property | Meaning |
| --- | --- |
| `status` | `complete`: no limit cut a stream and every member of every stream decoded and verified. `partial`: a limit cut a stream, a member did not decode, a member decoded without its checksum matching, a member was rebuilt by structural recovery, or the element table decoded no row. `failed`: the file could not be read at all (exit codes 2 and 4, and 3 for a stream over the platform array length). A read cut by a limit that is not accepted (exit code 3) is `partial` and has no `export` property, because nothing was exported. |
| `status` | `complete`: no limit cut a stream and every member of every stream decoded and verified. `partial`: a limit cut a stream, a member did not decode, a member decoded without its checksum matching, a member was rebuilt by structural recovery, or the element table decoded no row. `failed`: the file could not be read at all (exit codes 2 and 4, and 3 for a stream over the platform array length). A read cut by a limit that is not accepted (exit code 3) or a partial read refused by `--strict` (exit code 6) is `partial` and has no `export` property, because nothing was exported. |
| `exitCode` | The exit code of the process. |
| `input.file`, `input.bytes` | File name (no directory) and size of the input. |
| `error` | Only when `status` is `failed`: `kind` (`cannot-open`, `empty-file`, `not-a-compound-file`, `truncated-or-damaged`, `limit-exceeded`, `decode-failure`), `message`, and for `decode-failure` the `detail` with the stack trace. |
| `limits.raised`, `limits.partialAllowed` | Whether `--raise-limits` and `--allow-partial` were given. |
| `limits.raised`, `limits.partialAllowed`, `limits.strict` | Whether `--raise-limits`, `--allow-partial` and `--strict` were given. |
| `limits.hit[]` | Every stream cut by a limit: `stream`, `limit` (code from the table above), `value` (the limit in force), `raisable`, `detail`. |
| `streams[]` | The partition, Global, Formats and element table streams. `members` are the gzip members the stream is cut into; `decoded` produced output; `failed` produced none and their data is missing (a stream that holds bytes but not one gzip member is one failed member); `recovered` were rescued by a structural relock; `unverified` were kept although their checksum did not match or was never checked, which includes every rescued member. `decodedFraction` is `decoded / members`. |
| `elements.inTable`, `elements.decoded` | Rows of the element table, and those with a parsed record. `decodedFraction` is their quotient (1 for an empty table). |
| `issues[]` | Other findings that make a read `partial`, as `stream`, `code`, `detail`. The one code is `no-rows`: the element table decoded no row (a project always has elements, so the table did not decode). |
| `issues[]` | Findings that make a read `partial`, as `stream`, `code`, `detail`: `failed-members` (one or more gzip members of the stream produced no output), `recovered-members` (one or more members were rebuilt by structural recovery), `unverified-members` (one or more members decoded without a matching checksum), and `no-rows` (`Global/ElemTable` decoded no row; a project always has elements, so the table did not decode). |
| `elements.undecoded[]` | Rows of the element table without a parsed record, as `class`, `reason`, `count`, `examples`. The class of an element that never decoded is `unknown`. |
| `export.format` | `ifc`, `json` (the compact export) or `json-full`. |
| `export.considered`, `exported`, `skipped` | Parsed elements the export looked at, wrote, and left out, counted as the exporter counts them (`exported` counts elements, not product entities: for example, a roof element that writes two product entities, an `IfcRoof` and an `IfcSlab` with the same tag, counts once in `exported`, so the exported total can be below the number of product entities). `exportedFraction` is `exported / considered`. |
Expand All @@ -91,11 +93,12 @@ The compact export writes the rows of the element table: `block-only-record` and
## Scripting

```
rvtio project.rvt project.ifc --report project.report.json
rvtio project.rvt project.ifc --strict --report project.report.json
case $? in
0) ;; # done; check "status" in the report if a partial read matters
0) ;; # complete read and export written
2) echo "the file is cut or damaged" ;;
3) echo "limit exceeded: retry with --raise-limits" ;;
4) echo "the content could not be decoded" ;;
6) echo "the read was partial: see streams and issues in project.report.json" ;;
esac
```
2 changes: 1 addition & 1 deletion docs/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ projects through `InternalsVisibleTo`.
| `RvtReaderOptions` | Options of a read, including `DiagnosticOutput`, `ResolveCurrentCopies` (keeps only the current copy of an element that several partition streams hold; default true, set it to false to decode every copy), `RaiseLimits` and `Diagnostics`. `RevitLocalizationDirectory` is `[Obsolete]` and ignored: the texts are built in. |
| `RvtReadDiagnostics`, `StreamRecord`, `ReaderLimitHit` | What a read collects when `RvtReaderOptions.Diagnostics` is set: the partition streams read and the streams cut by a limit. |
| `ExportReport`, `SkipLog` | The machine-readable report of a run (`ToJson`) and the elements an export left out; see [the export report](export-report.md). |
| `RvtExitCodes`, `RvtInputException`, `RvtLimitException`, `RvtContainerCheck` | The exit codes 0 to 4 of the command line tool (code 5 is returned only by `downgrade --check`), the exceptions `RvtFileReader.Read` throws for a file it cannot read, and the check that raises them. |
| `RvtExitCodes`, `RvtInputException`, `RvtLimitException`, `RvtContainerCheck` | The exit codes 0 to 4 and 6 of the command line tool (code 5 is returned only by `downgrade --check`), the exceptions `RvtFileReader.Read` throws for a file it cannot read, and the check that raises them. |
| `FilteredElementCollector` | Query over the elements of a document (`OfCategory`, `WhereElementIsNotElementType`, `Where`, ...). |

## Model
Expand Down
25 changes: 20 additions & 5 deletions src/RvtIO.Cli/ExportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ private static int Export(ExportArguments arguments)
};

// --report <json>: the machine-readable report of the run; --allow-partial: export a read that a reader limit cut (exit 0 instead of 3);
// --strict: fail when a read is partial even without hitting a limit (exit 6 instead of 0);
// --raise-limits: raise the limits that cut a damaged stream while it is recovered (very large models)
bool allowPartial = args.Contains("--allow-partial");
bool strict = args.Contains("--strict");
bool raiseLimits = args.Contains("--raise-limits");
if (allowPartial && strict)
{
Console.Error.WriteLine("--allow-partial and --strict exclude each other.");
return RvtExitCodes.Usage;
}
string? reportPath = ValueOf(args, "--report");
if (args.Contains("--report") && (reportPath is null or "" || reportPath.StartsWith("--", StringComparison.Ordinal)))
{
Expand All @@ -57,32 +64,40 @@ private static int Export(ExportArguments arguments)
try { rvtInfo = reader.Read(rvtPath); }
catch (RvtInputException ex)
{
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, ex.Kind, ex.Message, RvtExitCodes.InputError, readDiagnostics, raiseLimits, allowPartial),
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, ex.Kind, ex.Message, RvtExitCodes.InputError, readDiagnostics, raiseLimits, allowPartial, null, strict),
$"error: {ex.Message} Nothing was exported (exit code {RvtExitCodes.InputError}).");
}
catch (RvtLimitException ex)
{
readDiagnostics.LimitHits.Add(ex.Hit);
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, "limit-exceeded", ex.Message, RvtExitCodes.LimitExceeded, readDiagnostics, raiseLimits, allowPartial),
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, "limit-exceeded", ex.Message, RvtExitCodes.LimitExceeded, readDiagnostics, raiseLimits, allowPartial, null, strict),
$"error: {ex.Message}. The limit cannot be raised. Nothing was exported (exit code {RvtExitCodes.LimitExceeded}).");
}
// The container is readable, so this is a decoder failure on damaged or unsupported content: one line on the console, the stack trace in the report.
catch (Exception ex) when (ex is not OutOfMemoryException)
{
string message = $"The content of '{inputName}' could not be decoded: {ex.GetType().Name}: {ex.Message}";
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, "decode-failure", message, RvtExitCodes.DecodeError, readDiagnostics, raiseLimits, allowPartial, ex.ToString()),
return EndRun(reportPath, ExportReport.Failed(inputName, inputBytes, "decode-failure", message, RvtExitCodes.DecodeError, readDiagnostics, raiseLimits, allowPartial, ex.ToString(), strict),
$"error: {message}. Nothing was exported (exit code {RvtExitCodes.DecodeError}). The report (--report) holds the stack trace.");
}
Console.Error.WriteLine($"Timing: read {swRead.Elapsed.TotalSeconds:F1}s");
var report = ExportReport.ForRead(inputName, inputBytes, readDiagnostics, raiseLimits, allowPartial,
reader.LastParsingState?.ElemTable.Keys.ToList() ?? [], rvtInfo);
reader.LastParsingState?.ElemTable.Keys.ToList() ?? [], rvtInfo, strict);
foreach (var hit in readDiagnostics.LimitHits)
Console.Error.WriteLine($"warning: stream {hit.Stream} was cut by the reader limit {hit.Limit} ({hit.Value.ToString("N0", System.Globalization.CultureInfo.InvariantCulture)}): {hit.Detail}");
if (report.ExitCode != RvtExitCodes.Success)
if (report.ExitCode == RvtExitCodes.LimitExceeded)
return EndRun(reportPath, report,
$"error: a stream exceeded a reader limit and the read was cut. Nothing was exported (exit code {report.ExitCode}). "
+ (raiseLimits ? "The limits are already raised; use --allow-partial to export what was read."
: "Use --raise-limits to raise the limits, or --allow-partial to export what was read."));
if (report.ExitCode == RvtExitCodes.PartialRead)
{
string reasons = report.Issues.Count > 0
? string.Join("; ", report.Issues.Select(issue => $"{issue.Stream}: {issue.Detail}"))
: "partial stream decode";
return EndRun(reportPath, report,
$"error: the read of '{inputName}' was partial ({reasons}). Nothing was exported (exit code {report.ExitCode}). Omit --strict to export what was read.");
}

if (outputPath.EndsWith(".ifc", StringComparison.OrdinalIgnoreCase))
{
Expand Down
Loading
Loading