@styx-api/core 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export { ArgtypeBackend } from "./argtype.js";
2
+ export { generateArgtype } from "./emit.js";
@@ -8,6 +8,7 @@ export type {
8
8
  EmitWarning,
9
9
  TypeMap,
10
10
  } from "./backend.js";
11
+ export { ArgtypeBackend, generateArgtype } from "./argtype/index.js";
11
12
  export { BoutiquesBackend, generateBoutiques } from "./boutiques/index.js";
12
13
  export { CodeBuilder } from "./code-builder.js";
13
14
  export type { FieldInfo } from "./collect-field-info.js";
@@ -0,0 +1,103 @@
1
+ ---
2
+ exe: "bet"
3
+ version: "6.0.4"
4
+ authors:
5
+ - "FMRIB Analysis Group, University of Oxford"
6
+ urls:
7
+ - "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki"
8
+ extensions:
9
+ - outputs
10
+ ---
11
+
12
+ /// Automated brain extraction tool for FSL
13
+ bet: seq(
14
+ /// Input image (e.g. img.nii.gz).
15
+ infile: path,
16
+
17
+ /// Output brain mask prefix (e.g. img_bet).
18
+ maskfile: str = "img_bet",
19
+
20
+ set(
21
+ /// Fractional intensity threshold (0->1). Smaller values give larger
22
+ /// brain outline estimates.
23
+ opt("-f", fractional_intensity: float.min(0).max(1).default(0.5)),
24
+
25
+ /// Vertical gradient in fractional intensity threshold (-1->1).
26
+ /// Positive values give larger brain outline at bottom, smaller at top.
27
+ opt("-g", vg_fractional_intensity: float.min(-1).max(1).default(0)),
28
+
29
+ /// XYZ coordinates (voxels, not mm) of the center of gravity of the
30
+ /// initial mesh surface.
31
+ opt("-c", center_of_gravity: rep(float).count(3)),
32
+
33
+ /// Head radius (mm, not voxels); initial surface sphere is set to half
34
+ /// of this.
35
+ opt("-r", head_radius: float),
36
+
37
+ /// Generate brain surface outline overlaid onto original image.
38
+ overlay: opt("-o").output(overlay_file: `{maskfile}_overlay.nii.gz`),
39
+
40
+ /// Generate binary brain mask.
41
+ binary_mask_flag: opt("-m").output(binary_mask: `{maskfile}_mask.nii.gz`),
42
+
43
+ /// Generate rough skull image (not as clean as betsurf).
44
+ approx_skull: opt("-s").output(
45
+ approx_skull_img: `{maskfile}_skull.nii.gz`,
46
+ skull_mask: `{maskfile}_skull_mask.nii.gz`,
47
+ ),
48
+
49
+ /// Don't generate segmented brain image output.
50
+ no_seg_output: opt("-n"),
51
+
52
+ /// Generate brain surface as mesh in .vtk format.
53
+ vtk_mesh: opt("-e").output(output_vtk_mesh: `{maskfile}_mesh.vtk`),
54
+
55
+ /// Apply thresholding to segmented brain image and mask.
56
+ thresholding: opt("-t"),
57
+
58
+ /// More robust brain center estimation by iterating BET with a
59
+ /// changing center-of-gravity.
60
+ robust_iters: opt("-R"),
61
+
62
+ /// Cleanup residual eye and optic nerve voxels (useful before
63
+ /// SIENA/SIENAX).
64
+ residual_optic_cleanup: opt("-S"),
65
+
66
+ /// Reduce image bias and residual neck voxels (useful before
67
+ /// SIENA/SIENAX).
68
+ reduce_bias: opt("-B"),
69
+
70
+ /// Pad end slices in both directions; useful when only a few slices
71
+ /// are present in the Z direction.
72
+ slice_padding: opt("-Z"),
73
+
74
+ /// Determine brain mask from the first volume of a 4D dataset and
75
+ /// apply it to the whole set. Intended for FMRI; the -f threshold
76
+ /// is reduced to 0.3.
77
+ whole_set_mask: opt("-F"),
78
+
79
+ /// Run bet2 + betsurf so skull and scalp surfaces are also produced
80
+ /// (requires registration to standard space).
81
+ additional_surfaces: opt("-A").output(
82
+ out_inskull_mask: `{maskfile}_inskull_mask.nii.gz`,
83
+ out_inskull_mesh: `{maskfile}_inskull_mesh.nii.gz`,
84
+ out_inskull_off: `{maskfile}_inskull_mesh.off`,
85
+ out_outskin_mask: `{maskfile}_outskin_mask.nii.gz`,
86
+ out_outskin_mesh: `{maskfile}_outskin_mesh.nii.gz`,
87
+ out_outskin_off: `{maskfile}_outskin_mesh.off`,
88
+ out_outskull_mask: `{maskfile}_outskull_mask.nii.gz`,
89
+ out_outskull_mesh: `{maskfile}_outskull_mesh.nii.gz`,
90
+ out_outskull_off: `{maskfile}_outskull_mesh.off`,
91
+ ),
92
+
93
+ /// Same as -A, but also takes a T2 image as input to refine the
94
+ /// skull/scalp surfaces.
95
+ opt("-A2", additional_surfaces_t2: path),
96
+
97
+ /// Switch on diagnostic messages.
98
+ verbose: opt("-v"),
99
+
100
+ /// Don't delete temporary intermediate images.
101
+ debug: opt("-d"),
102
+ ),
103
+ ).output(outfile: `{maskfile}.nii.gz`)
@@ -0,0 +1,101 @@
1
+ /**
2
+ * AST for the argtype sugar DSL. Mirrors the grammar in the argtype spec
3
+ * (combinators, terminals, literals, naming, aliases, method chains) and is the
4
+ * intermediate the parser produces before `lower.ts` translates it to Styx IR.
5
+ *
6
+ * Decorations (`name`, `doc`, `default`, value constraints, `.join`/`.count`,
7
+ * outputs, media types) attach to any node, so they live as optional fields on
8
+ * the single `AstNode` shape rather than as wrapper nodes - lowering folds them
9
+ * onto the corresponding `NodeMeta` / terminal attrs.
10
+ */
11
+
12
+ /** A combinator keyword. `set`/`any` have no direct IR node (see `lower.ts`). */
13
+ export type Combinator = "seq" | "set" | "opt" | "rep" | "alt" | "any";
14
+
15
+ /** A terminal keyword. */
16
+ export type Terminal = "int" | "float" | "str" | "path";
17
+
18
+ export interface AstNode {
19
+ kind: "literal" | "terminal" | "comb" | "ref";
20
+
21
+ /** kind === "literal": the fixed token string. */
22
+ value?: string;
23
+ /** kind === "terminal": which terminal. */
24
+ terminal?: Terminal;
25
+ /** kind === "comb": which combinator + its children. */
26
+ op?: Combinator;
27
+ children?: AstNode[];
28
+ /** kind === "ref": an alias reference (resolved by substitution). */
29
+ refName?: string;
30
+
31
+ // -- Decorations (any node) --
32
+
33
+ /** `label:` sugar / `.name("...")`. */
34
+ name?: string;
35
+ /** Title: a `# ` heading in a `///` block, or `.title("...")`. */
36
+ title?: string;
37
+ /** Description: a `///` block (minus the title), or `.description("...")`. */
38
+ description?: string;
39
+ /** `= value` sugar / `.default(...)` (terminal only). */
40
+ default?: string | number;
41
+ /** `.min(n)` (int/float only). */
42
+ min?: number;
43
+ /** `.max(n)` (int/float only). */
44
+ max?: number;
45
+ /** `.join(sep?)` - present means joined; separator defaults to "". */
46
+ join?: string;
47
+ /** `.count(n)` (= `.countMin(n).countMax(n)`) / `.countMin(n)` / `.countMax(n)`
48
+ * (rep only). */
49
+ countMin?: number;
50
+ countMax?: number;
51
+ /** `.mediaType(mime)` (path only; `mediatypes` extension). */
52
+ mediaTypes?: string[];
53
+ /** `.mutable()` (path only; `paths` extension). */
54
+ mutable?: boolean;
55
+ /** `.resolveParent()` (path only; `paths` extension). */
56
+ resolveParent?: boolean;
57
+ /** `.output(...)` (`outputs` extension). */
58
+ outputs?: AstOutput[];
59
+ }
60
+
61
+ /** One output template declared via `.output(...)`. */
62
+ export interface AstOutput {
63
+ /** `label:` on the template / `.name(...)`. */
64
+ name?: string;
65
+ /** Title of the produced file (from a `# ` heading in the entry's `///`). */
66
+ title?: string;
67
+ /** Description of the produced file (from the entry's `///`). */
68
+ description?: string;
69
+ /** Output-level `.or(fallback)` on the template. */
70
+ fallback?: string;
71
+ tokens: AstOutputToken[];
72
+ }
73
+
74
+ /** A piece of an output path template: literal text or a `{ref}` interpolation. */
75
+ export type AstOutputToken =
76
+ | { kind: "literal"; value: string }
77
+ | {
78
+ kind: "ref";
79
+ /** Target node name; undefined for a self-reference (`{}`). */
80
+ name?: string;
81
+ stripSuffix?: string[];
82
+ stripPrefix?: string[];
83
+ basename?: boolean;
84
+ or?: string;
85
+ };
86
+
87
+ /** A top-level alias definition (`Name = expr`). */
88
+ export interface AstAlias {
89
+ name: string;
90
+ expr: AstNode;
91
+ }
92
+
93
+ /** The result of parsing a full argtype document. */
94
+ export interface AstDocument {
95
+ frontmatter?: Record<string, unknown>;
96
+ aliases: AstAlias[];
97
+ /** The root definition's name (`name: expr`). Undefined for an anonymous root
98
+ * (a bare top-level expression); the tool id then falls back to frontmatter. */
99
+ rootName?: string;
100
+ root: AstNode;
101
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Doc-comment title/description handling.
3
+ *
4
+ * The `# Title` heading convention is *sugar*: it applies only when splitting a
5
+ * `///` block (`splitDocText`). The chaining methods `.title()` / `.description()`
6
+ * set the title / description directly and are not re-parsed for headings.
7
+ */
8
+
9
+ export interface DocParts {
10
+ title?: string;
11
+ description?: string;
12
+ }
13
+
14
+ /**
15
+ * Reflow a `///` description as Markdown-style prose: a single line break is a
16
+ * soft wrap (the lines join with a space) and a blank line starts a new
17
+ * paragraph (kept as `\n\n`). This lets an author wrap a long description across
18
+ * several `///` lines for readability without forcing hard breaks, while real
19
+ * paragraph structure survives. (It does not preserve Markdown lists or indented
20
+ * code blocks - their single line breaks are joined like ordinary prose.)
21
+ */
22
+ function reflowProse(text: string): string {
23
+ return text
24
+ .split(/\n{2,}/) // blank line(s) -> paragraph boundary
25
+ .map((para) =>
26
+ para
27
+ .split("\n")
28
+ .map((line) => line.trim())
29
+ .filter((line) => line.length > 0)
30
+ .join(" "),
31
+ )
32
+ .filter((para) => para.length > 0)
33
+ .join("\n\n");
34
+ }
35
+
36
+ /**
37
+ * Split a `///` doc block into title + description. A leading Markdown H1
38
+ * (`# Title` on the first line) is the title; everything after it is the
39
+ * description. Without a leading `# `, the whole block is the description (no
40
+ * title). The description is reflowed as prose (see `reflowProse`): single line
41
+ * breaks soft-wrap, blank lines separate paragraphs.
42
+ */
43
+ export function splitDocText(raw: string): DocParts {
44
+ const newline = raw.indexOf("\n");
45
+ const firstLine = (newline === -1 ? raw : raw.slice(0, newline)).trim();
46
+ if (firstLine.startsWith("# ")) {
47
+ const title = firstLine.slice(2).trim();
48
+ const description = reflowProse(newline === -1 ? "" : raw.slice(newline + 1));
49
+ return {
50
+ ...(title && { title }),
51
+ ...(description && { description }),
52
+ };
53
+ }
54
+ const description = reflowProse(raw);
55
+ return description ? { description } : {};
56
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Frontmatter handling for argtype documents.
3
+ *
4
+ * A document may begin with a `---` fenced block of YAML-ish metadata (`exe`,
5
+ * `version`, `extensions`, `container`, `authors`, `stdout`, ...). Rather than
6
+ * pull in a full YAML dependency, this parses the small subset the format uses:
7
+ * scalar `key: value`, block sequences (`- item`), and one level of nested
8
+ * mappings. That covers every key the lowering step consumes; anything more
9
+ * exotic is read as a raw string and ignored downstream.
10
+ */
11
+
12
+ export interface SplitResult {
13
+ frontmatter?: Record<string, unknown>;
14
+ /** The document body. Frontmatter lines are blanked (not removed) so token
15
+ * line numbers still line up with the original source. */
16
+ body: string;
17
+ errors: string[];
18
+ }
19
+
20
+ export function splitFrontmatter(source: string): SplitResult {
21
+ // Tolerate a leading BOM / blank lines before the opening fence.
22
+ const lines = source.split("\n");
23
+ let start = 0;
24
+ while (start < lines.length && lines[start]!.trim() === "") start++;
25
+
26
+ if (lines[start]?.trim() !== "---") {
27
+ return { body: source, errors: [] };
28
+ }
29
+
30
+ // Find the closing fence.
31
+ let end = -1;
32
+ for (let i = start + 1; i < lines.length; i++) {
33
+ if (lines[i]!.trim() === "---") {
34
+ end = i;
35
+ break;
36
+ }
37
+ }
38
+ if (end === -1) {
39
+ return { body: source, errors: ["Unterminated frontmatter block (missing closing '---')"] };
40
+ }
41
+
42
+ const fmLines = lines.slice(start + 1, end);
43
+ const { value, errors } = parseYamlish(fmLines);
44
+
45
+ // Blank out the frontmatter region (fences + content) to preserve line numbers.
46
+ const blanked = lines.map((line, i) => (i >= start && i <= end ? "" : line));
47
+ return { frontmatter: value, body: blanked.join("\n"), errors };
48
+ }
49
+
50
+ interface Line {
51
+ indent: number;
52
+ content: string;
53
+ }
54
+
55
+ function tokenizeLines(raw: string[]): Line[] {
56
+ const out: Line[] = [];
57
+ for (const line of raw) {
58
+ const noComment = stripComment(line);
59
+ if (noComment.trim() === "") continue;
60
+ const indent = noComment.length - noComment.trimStart().length;
61
+ out.push({ indent, content: noComment.trim() });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** Strip a trailing `#` comment that is not inside quotes. Following YAML, a
67
+ * `#` only starts a comment at the start of the line or after whitespace, so an
68
+ * unquoted value containing `#` (e.g. a URL fragment `https://x/#frag`) is kept
69
+ * intact. */
70
+ function stripComment(line: string): string {
71
+ let inQuote: string | null = null;
72
+ for (let i = 0; i < line.length; i++) {
73
+ const ch = line[i]!;
74
+ // A backslash escape inside a double-quoted value hides the next char (so
75
+ // `\"` does not close the string).
76
+ if (inQuote === '"' && ch === "\\") {
77
+ i++;
78
+ continue;
79
+ }
80
+ if (inQuote) {
81
+ if (ch === inQuote) inQuote = null;
82
+ } else if (ch === '"' || ch === "'") {
83
+ inQuote = ch;
84
+ } else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === "\t")) {
85
+ return line.slice(0, i);
86
+ }
87
+ }
88
+ return line;
89
+ }
90
+
91
+ function parseYamlish(raw: string[]): { value: Record<string, unknown>; errors: string[] } {
92
+ const lines = tokenizeLines(raw);
93
+ const errors: string[] = [];
94
+ let pos = 0;
95
+
96
+ function parseBlock(minIndent: number): Record<string, unknown> | unknown[] {
97
+ // Sequence block?
98
+ if (
99
+ pos < lines.length &&
100
+ lines[pos]!.indent >= minIndent &&
101
+ lines[pos]!.content.startsWith("- ")
102
+ ) {
103
+ const seq: unknown[] = [];
104
+ const indent = lines[pos]!.indent;
105
+ while (
106
+ pos < lines.length &&
107
+ lines[pos]!.indent === indent &&
108
+ lines[pos]!.content.startsWith("- ")
109
+ ) {
110
+ seq.push(parseScalar(lines[pos]!.content.slice(2).trim()));
111
+ pos++;
112
+ }
113
+ return seq;
114
+ }
115
+
116
+ // Mapping block.
117
+ const map: Record<string, unknown> = {};
118
+ if (pos >= lines.length) return map;
119
+ const indent = lines[pos]!.indent;
120
+ while (
121
+ pos < lines.length &&
122
+ lines[pos]!.indent === indent &&
123
+ !lines[pos]!.content.startsWith("- ")
124
+ ) {
125
+ const { content } = lines[pos]!;
126
+ const colon = findColon(content);
127
+ if (colon === -1) {
128
+ errors.push(`Malformed frontmatter line: '${content}'`);
129
+ pos++;
130
+ continue;
131
+ }
132
+ const key = content.slice(0, colon).trim();
133
+ const valueText = content.slice(colon + 1).trim();
134
+ pos++;
135
+ if (valueText !== "") {
136
+ map[key] = parseScalar(valueText);
137
+ } else if (pos < lines.length && lines[pos]!.indent > indent) {
138
+ map[key] = parseBlock(lines[pos]!.indent);
139
+ } else {
140
+ map[key] = null;
141
+ }
142
+ }
143
+ return map;
144
+ }
145
+
146
+ const value = parseBlock(0);
147
+ return { value: isRecord(value) ? value : { _root: value }, errors };
148
+ }
149
+
150
+ function findColon(s: string): number {
151
+ let inQuote: string | null = null;
152
+ for (let i = 0; i < s.length; i++) {
153
+ const ch = s[i]!;
154
+ if (inQuote === '"' && ch === "\\") {
155
+ i++;
156
+ continue;
157
+ }
158
+ if (inQuote) {
159
+ if (ch === inQuote) inQuote = null;
160
+ } else if (ch === '"' || ch === "'") {
161
+ inQuote = ch;
162
+ } else if (ch === ":") {
163
+ return i;
164
+ }
165
+ }
166
+ return -1;
167
+ }
168
+
169
+ /** Reverse the backslash escapes an emitter applies to a quoted scalar
170
+ * (`\n`, `\t`, `\r`, `\"`, `\\`), so values with newlines/quotes round-trip. */
171
+ function unescapeScalar(s: string): string {
172
+ return s.replace(/\\(.)/g, (_, c) =>
173
+ c === "n" ? "\n" : c === "t" ? "\t" : c === "r" ? "\r" : c,
174
+ );
175
+ }
176
+
177
+ function parseScalar(text: string): unknown {
178
+ if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
179
+ return unescapeScalar(text.slice(1, -1));
180
+ }
181
+ if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) {
182
+ return text.slice(1, -1);
183
+ }
184
+ if (text === "true") return true;
185
+ if (text === "false") return false;
186
+ if (text === "null" || text === "~") return null;
187
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
188
+ return text;
189
+ }
190
+
191
+ function isRecord(x: unknown): x is Record<string, unknown> {
192
+ return typeof x === "object" && x !== null && !Array.isArray(x);
193
+ }
@@ -0,0 +1 @@
1
+ export { ArgtypeParser } from "./parser-frontend.js";