agent-cfml-check 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Current status
6
6
 
7
- Version `0.1.1` contains the first feasibility slice plus executable schema validation, package exports, and Agent onboarding. The prior `agent-cfml-check@0.1.0` publication predates these additions. The current implementation is verified for:
7
+ Version `0.2.0` extends the `0.1.1` structural profile with parameter and exception tags. The implementation checks:
8
8
 
9
9
  - paired CFML tag nesting;
10
10
  - `cfelse` and `cfelseif` ownership and ordering;
@@ -17,7 +17,14 @@ Version `0.1.1` contains the first feasibility slice plus executable schema vali
17
17
  - explicit-root path safety, source stability checks, input validation, output limits, nesting limits, finding limits, and time limits;
18
18
  - deterministic JSON and text CLI output.
19
19
 
20
- Local verification before this documentation update:
20
+ Current repository source also recognizes `cfparam` as a bodyless tag and checks paired `cftry`, `cfcatch`, and `cffinally` structure. These changes are not included in the published `0.1.1` artifact or existing installed snapshots. They do not validate tag attributes, exception behavior, or runtime semantics. Self-closing paired tags, including `cfcatch`, return incomplete rather than an engine-validity claim.
21
+
22
+ The current local source corrects CFScript identifiers containing `cf` being
23
+ misclassified as markup. Ordinary variables and calls remain script text; actual
24
+ CFML tag islands still return `incomplete` with their source position. This fix
25
+ is included in the `0.2.0` source.
26
+
27
+ Historical verification for the `0.1.1` release baseline:
21
28
 
22
29
  - `npm test`: 22/22 passed;
23
30
  - `npm run typecheck`: passed;
@@ -27,10 +34,22 @@ Local verification before this documentation update:
27
34
  - targeted Lucee `6.2.2.91` execution probe: passed;
28
35
  - Ubuntu WSL Node `18.19.1` `npm ci`, tests, and typecheck: passed.
29
36
 
30
- The prior npm publication of `agent-cfml-check@0.1.0` is immutable and predates this release's schema, package-export, and documentation updates. This release targets `agent-cfml-check@0.1.1`; authenticated registry publication/readback is the final release step. This is not a complete CFML parser or evidence of Adobe ColdFusion compatibility. A targeted Lucee `6.2.2.91` probe and an Ubuntu WSL Node `18.19.1` clean dependency install/test run pass; these do not establish a full engine matrix. Hub lifecycle, canonical ownership, admission, and Adobe compatibility remain outside the current verified evidence.
37
+ This is not a complete CFML parser or evidence of Adobe ColdFusion compatibility. A targeted Lucee `6.2.2.91` probe and an Ubuntu WSL Node `18.19.1` run for prior source do not establish a full engine or platform matrix for `0.2.0`.
31
38
 
32
39
  ## Install and use
33
40
 
41
+ Install the published CLI globally for Codex CLI or any terminal session:
42
+
43
+ ```sh
44
+ npm install --global agent-cfml-check@latest
45
+ agent-cfml-check capabilities --json
46
+ agent-cfml-check check --root /path/to/repository path/to/file.cfm --json
47
+ ```
48
+
49
+ The global installation exposes the `agent-cfml-check` command from any working
50
+ directory. For a checkout, use `npm ci`, `npm run build`, and the same command
51
+ through `node dist/cli/index.js`.
52
+
34
53
  ```sh
35
54
  npm install
36
55
  npm test
@@ -52,6 +71,11 @@ agent-cfml-check check --root <directory> <file> [--json] [--pretty]
52
71
 
53
72
  Check limits can be overridden with `--max-source-bytes`, `--max-nesting`, `--max-findings`, `--max-output-bytes`, and `--time-limit-ms`, subject to the hard caps reported by `capabilities`.
54
73
 
74
+ `max_output_bytes` must be at least 512, reserving a complete failure envelope.
75
+ CLI ceilings count actual UTF-8 stdout, including indentation and the final
76
+ newline. If pretty output cannot fit, checking returns incomplete/exit 3 and may
77
+ emit its failure envelope as compact JSON. Default fitting output is unchanged.
78
+
55
79
  The checker requires one explicit root and one `.cfm` or `.cfc` file. JSON mode emits one envelope on stdout; diagnostics stay on stderr. A completed check with structural violations still exits `0` and reports `data.verdict: "violations"`. Exit `3` means unsupported syntax, insufficient evidence, or a resource limit. Exit `4` means an explicit-root or access-policy rejection. See [SPEC.md](SPEC.md) for the complete contract.
56
80
 
57
81
  ## Safety and boundaries
@@ -76,4 +100,4 @@ The supported profile is `cfml-structure-v1`. Unknown/custom/imported tags, opti
76
100
  - `dist/`: generated package output;
77
101
  - `DESIGN.md`, `EPIC.md`, `ROADMAP.md`, `TASK.md`, `GOAL.md`, `PROGRESS.md`, `GOAL_PROMPT.md`, `CHANGELOG.md`: repository design, planning, status, and maintenance documents.
78
102
 
79
- `package.json` currently publishes `dist`, `README.md`, `SPEC.md`, `schema`, and `fixtures`; the design, planning, progress, goal, prompt, changelog, and Agent Skill documents remain repository-maintainer documentation. The repository exposes the TypeScript library through `main`, `types`, and `exports`, with package-surface and packed-install tests. The release package version is `0.1.1`.
103
+ `package.json` includes `dist`, `README.md`, `SPEC.md`, `schema`, and `fixtures` in the package; the design, planning, progress, goal, prompt, changelog, and Agent Skill documents remain repository-maintainer documentation. The repository exposes the TypeScript library through `main`, `types`, and `exports`. The source package version is `0.2.0`.
package/SPEC.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent CFML Check Specification
2
2
 
3
3
  **Status:** frozen feasibility contract for implementation baseline `c76b8be`, extended by release hardening
4
- **Tool version:** `0.1.1`
4
+ **Tool version:** `0.2.0`
5
5
  **Profile:** `cfml-structure-v1`
6
6
  **Envelope schema:** `schema/agent-cfml-check-result-v1.schema.json`
7
7
 
@@ -22,11 +22,11 @@ agent-cfml-check check --root <directory> <file> [--json] [--pretty]
22
22
 
23
23
  `capabilities` reports the profile, operations, extensions, tag catalogue, effective default limits, and exclusions. The CLI does not accept a file, root, or check-limit override for this operation.
24
24
 
25
- `check` requires exactly one explicit root and one `.cfm` or `.cfc` file. `--pretty` requires `--json`. `--help` prints usage and `--version` prints `0.1.1`; neither may be combined with other options.
25
+ `check` requires exactly one explicit root and one `.cfm` or `.cfc` file. `--pretty` requires `--json`. `--help` prints usage and `--version` prints `0.2.0`; neither may be combined with other options.
26
26
 
27
27
  ### TypeScript interface
28
28
 
29
- `src/index.ts` exports `capabilities`, `checkFile`, `DEFAULT_LIMITS`, `HARD_LIMITS`, `normalizeLimits`, and schema types. Repository package metadata declares `main: "./dist/index.js"`, `types: "./dist/index.d.ts"`, and an import/types `exports` map. Package-surface tests and a temporary packed-install CLI/library smoke check verify the current `0.1.1` artifact.
29
+ `src/index.ts` exports `capabilities`, `checkFile`, `DEFAULT_LIMITS`, `HARD_LIMITS`, `normalizeLimits`, and schema types. Repository package metadata declares `main: "./dist/index.js"`, `types: "./dist/index.d.ts"`, and an import/types `exports` map. Package-surface tests and a temporary packed-install CLI/library smoke check verified the previously published `0.1.1` artifact.
30
30
 
31
31
  The TypeScript `capabilities(limitsInput)` API may receive limit overrides, while the CLI `capabilities` operation rejects them.
32
32
 
@@ -34,15 +34,17 @@ The TypeScript `capabilities(limitsInput)` API may receive limit overrides, whil
34
34
 
35
35
  ### Paired bodies
36
36
 
37
- `cfcomponent`, `cffunction`, `cfif`, `cfloop`, `cfoutput`, `cfquery`, `cfsavecontent`, `cfscript`, and `cfsilent` require matching closing tags and cannot be self-closing.
37
+ `cfcatch`, `cfcomponent`, `cffinally`, `cffunction`, `cfif`, `cfloop`, `cfoutput`, `cfquery`, `cfsavecontent`, `cfscript`, `cfsilent`, and `cftry` require matching closing tags in this profile. Self-closing spellings of paired tags return incomplete; this does not assert that every CFML engine rejects them.
38
38
 
39
39
  ### Branches
40
40
 
41
41
  `cfelse` and `cfelseif` must belong to the directly open `cfif`. Only one `cfelse` is accepted, and `cfelseif` cannot follow `cfelse`.
42
42
 
43
+ `cfcatch` and `cffinally` must belong to the directly open `cftry`. A closed `cftry` must contain at least one direct `cfcatch` or `cffinally`; multiple catches are allowed, at most one `cffinally` is allowed, and catches cannot follow it. Nested `cftry` blocks have independent handlers. The scanner does not validate catch attributes, exception types, or arbitrary text and statements between handlers. Support for these tags is a source change after the published `0.1.1` artifact.
44
+
43
45
  ### Bodyless tags
44
46
 
45
- `cfargument`, `cfinclude`, `cfqueryparam`, `cfreturn`, and `cfset` are standalone. Ordinary and self-closing spellings are accepted. Closing a bodyless tag is unsupported.
47
+ `cfargument`, `cfinclude`, `cfparam`, `cfqueryparam`, `cfreturn`, and `cfset` are standalone. Ordinary and self-closing spellings are accepted. Closing a bodyless tag is unsupported. `cfparam` support is a source change after the published `0.1.1` artifact; it does not validate attributes or runtime semantics.
46
48
 
47
49
  Tag names are case-insensitive. Unknown, custom, imported, and unlisted tags are unsupported.
48
50
 
@@ -91,9 +93,18 @@ Check data contains the profile, source path/SHA-256/byte size/UTF-8/BOM metadat
91
93
 
92
94
  Requested limits must be positive safe integers and cannot exceed hard caps. Reaching a limit returns a fail-closed result rather than partial data.
93
95
 
96
+ `max_output_bytes` has a minimum of 512 to reserve the compact failure envelope
97
+ at every hard-limit combination. CLI accounting includes the selected rendering
98
+ and final newline. An oversized result is withheld as incomplete with exit 3;
99
+ pretty mode falls back to compact failure JSON if indentation cannot fit.
100
+
94
101
  ## 7. Input and safety behavior
95
102
 
96
- The source reader requires `.cfm` or `.cfc`, an existing root and file, realpath containment, a regular file, valid UTF-8, no NUL byte, and an effective source-size limit. It checks size and modification time before and after reading and rejects a changed snapshot. Symlink escapes are rejected.
103
+ The source reader requires `.cfm` or `.cfc`, an existing root and file, realpath containment, a regular file, valid UTF-8, no NUL byte, and an effective source-size limit. Static symlink escapes are rejected before admission.
104
+
105
+ Reading is bound to a regular-file descriptor opened with no-follow and nonblocking flags where supported. Before consuming bytes, the reader compares admitted and opened device/inode identity, mode, size and nanosecond modification/change timestamps, checks the current pathname state, and rechecks the canonical path. Observed leaf or ancestor substitutions return the existing `SOURCE_CHANGED`/incomplete/exit 3 result. The same descriptor and pathname state are checked after reading, and the descriptor is closed on success or rejection.
106
+
107
+ Oversized metadata is rejected before body reads. Actual reads are bounded to the admitted size plus one byte for growth detection and cannot exceed the selected source-byte limit plus one. Over-limit growth returns `LIMIT_EXCEEDED`/incomplete/exit 3 before UTF-8 decoding. These checks reject observed mutation; they do not provide an atomic filesystem snapshot or operating-system path confinement.
97
108
 
98
109
  Known envelope error codes include `INVALID_ARGUMENT`, `INVALID_LIMIT`, `UNSUPPORTED_EXTENSION`, `ROOT_NOT_FOUND`, `FILE_NOT_FOUND`, `PATH_UNRESOLVED`, `FILE_OUTSIDE_ROOT`, `LIMIT_EXCEEDED`, `SOURCE_CHANGED`, `BINARY_INPUT`, `ENCODING_UNSUPPORTED`, `UNSUPPORTED_SYNTAX`, and `INTERNAL_ERROR`.
99
110
 
@@ -111,6 +122,6 @@ JSON mode emits one envelope on stdout; failure diagnostics are also written to
111
122
 
112
123
  ## 9. Evidence boundary
113
124
 
114
- The profile does not validate full CFML grammar, expression/type/runtime semantics, HTML, SQL, include expansion, directory/project context, Lucee/Adobe execution, unknown/custom/imported tags, or CFML tag islands inside `cfscript`. The `agent-cfml-check@0.1.1` CLI package is prepared for publication; authenticated npm registry readback remains required. Repository package export metadata, executable schema validation, and temporary packed-install verification are complete. A targeted Lucee `6.2.2.91` probe and Ubuntu WSL Node `18.19.1` verification pass, but they do not establish a full engine matrix. Adobe ColdFusion, Hub lifecycle, canonical ownership/admission, and registry parity remain outside current verified evidence.
125
+ The profile does not validate full CFML grammar, expression/type/runtime semantics, HTML, SQL, include expansion, directory/project context, Lucee/Adobe execution, unknown/custom/imported tags, or CFML tag islands inside `cfscript`. The prior `0.1.1` artifact was published and registry-verified. A targeted Lucee `6.2.2.91` probe and Ubuntu WSL Node `18.19.1` verification for prior source do not establish a full engine matrix for `0.2.0`.
115
126
 
116
- The repository tests cover CF-01 through CF-13, pure-script `.cfc` handling, three CLI behaviors, and the package export surface. Current local evidence is 22/22 tests, typecheck pass, package dry-run pass, Windows CLI smoke checks, temporary packed-install CLI/library smoke checks, and JSON Schema validation. The prior immutable `0.1.0` artifact predates this release hardening; `0.1.1` registry readback remains required. Full Lucee/Adobe engine comparison, canonical Hub admission, and registry parity remain pending.
127
+ The repository tests cover CF-01 through CF-13, pure-script `.cfc` handling, CLI behavior, package exports, and the parameter and exception tag additions. Full Lucee/Adobe engine comparison and a broader platform matrix remain unverified.
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { capabilities, checkFile } from "../core/checker.js";
2
+ import { capabilities, checkFile, fitOutput } from "../core/checker.js";
3
3
  import { DEFAULT_LIMITS } from "../core/limits.js";
4
4
  import { ToolFailure } from "../schema/errors.js";
5
5
  import { TOOL_ID, TOOL_VERSION } from "../schema/types.js";
@@ -128,14 +128,21 @@ export function main(argv = process.argv.slice(2)) {
128
128
  const result = args.operation === "capabilities"
129
129
  ? { envelope: capabilities(args.limits), exit_code: 0 }
130
130
  : checkFile({ root: args.root, file: args.file, limits: args.limits });
131
- if (args.json)
132
- process.stdout.write(`${JSON.stringify(result.envelope, null, args.pretty ? 2 : undefined)}\n`);
133
- else
134
- process.stdout.write(renderText(result.envelope));
135
- if (result.envelope.status !== "ok")
136
- for (const error of result.envelope.errors)
131
+ const serialize = (value) => args.json
132
+ ? `${JSON.stringify(value, null, args.pretty ? 2 : undefined)}\n`
133
+ : renderText(value);
134
+ const envelope = args.operation === "check"
135
+ ? fitOutput(result.envelope, result.envelope.meta.limits, serialize)
136
+ : result.envelope;
137
+ let output = serialize(envelope);
138
+ // A compact failure retains the complete envelope when indentation cannot fit.
139
+ if (args.json && Buffer.byteLength(output) > envelope.meta.limits.max_output_bytes)
140
+ output = `${JSON.stringify(envelope)}\n`;
141
+ process.stdout.write(output);
142
+ if (envelope.status !== "ok")
143
+ for (const error of envelope.errors)
137
144
  process.stderr.write(`error: ${error.code}: ${error.message}\n`);
138
- return result.exit_code;
145
+ return envelope.status === "incomplete" ? 3 : result.exit_code;
139
146
  }
140
147
  catch (error) {
141
148
  const failure = error instanceof ToolFailure ? error : new ToolFailure("INTERNAL_ERROR", "The CLI failed without a safe result", 1);
@@ -1,4 +1,5 @@
1
1
  import type { CapabilitiesData, CheckData, CheckLimits, Envelope, CheckOptions } from "../schema/types.js";
2
+ export declare function fitOutput<T>(envelope: Envelope<T>, limits: CheckLimits, serialize?: (value: Envelope<T>) => string): Envelope<T>;
2
3
  export declare function capabilities(limitsInput?: Partial<CheckLimits>): Envelope<CapabilitiesData>;
3
4
  export declare function checkFile(options: CheckOptions): {
4
5
  envelope: Envelope<CheckData>;
@@ -11,14 +11,12 @@ function meta(limits) {
11
11
  function emptyEnvelope(status, limits, errors) {
12
12
  return { schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status, complete: false, data: null, errors, warnings: [], meta: meta(limits) };
13
13
  }
14
- function fitOutput(envelope, limits) {
15
- const serialized = JSON.stringify(envelope);
14
+ export function fitOutput(envelope, limits, serialize = (value) => `${JSON.stringify(value)}\n`) {
15
+ const serialized = serialize(envelope);
16
16
  if (Buffer.byteLength(serialized, "utf8") <= limits.max_output_bytes)
17
17
  return envelope;
18
18
  const fallback = emptyEnvelope("incomplete", limits, [{ code: "LIMIT_EXCEEDED", message: "The output limit was reached; result data was withheld" }]);
19
- if (Buffer.byteLength(JSON.stringify(fallback), "utf8") <= limits.max_output_bytes)
20
- return fallback;
21
- return { schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status: "incomplete", complete: false, data: null, errors: [{ code: "LIMIT_EXCEEDED", message: "Output limit exceeded" }], warnings: [], meta: meta(limits) };
19
+ return fallback;
22
20
  }
23
21
  export function capabilities(limitsInput) {
24
22
  const limits = normalizeLimits(limitsInput);
@@ -82,13 +80,12 @@ export function checkFile(options) {
82
80
  ],
83
81
  findings: scanned.findings,
84
82
  };
85
- return {
86
- envelope: fitOutput({ schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status: "ok", complete: true, data, errors: [], warnings: [], meta: meta(limits) }, limits),
87
- exit_code: 0,
88
- };
83
+ const envelope = fitOutput({ schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status: "ok", complete: true, data, errors: [], warnings: [], meta: meta(limits) }, limits);
84
+ return { envelope, exit_code: envelope.status === "incomplete" ? 3 : 0 };
89
85
  }
90
86
  catch (error) {
91
87
  const failure = error instanceof ToolFailure ? error : new ToolFailure("INTERNAL_ERROR", "The checker failed without a safe result", 1);
92
- return { envelope: fitOutput(emptyEnvelope(failure.exitCode === 3 ? "incomplete" : "error", limits, [{ code: failure.code, message: failure.message }]), limits), exit_code: failure.exitCode };
88
+ const envelope = fitOutput(emptyEnvelope(failure.exitCode === 3 ? "incomplete" : "error", limits, [{ code: failure.code, message: failure.message }]), limits);
89
+ return { envelope, exit_code: envelope.status === "incomplete" ? 3 : failure.exitCode };
93
90
  }
94
91
  }
@@ -10,13 +10,18 @@ export const PAIRED_TAGS = new Set([
10
10
  "cfcomponent",
11
11
  "cffunction",
12
12
  "cfscript",
13
+ "cftry",
14
+ "cfcatch",
15
+ "cffinally",
13
16
  ]);
14
17
  export const BRANCH_TAGS = new Set(["cfelseif", "cfelse"]);
15
- export const BODYLESS_TAGS = new Set(["cfset", "cfreturn", "cfinclude", "cfargument", "cfqueryparam"]);
18
+ export const BODYLESS_TAGS = new Set(["cfset", "cfreturn", "cfinclude", "cfargument", "cfqueryparam", "cfparam"]);
16
19
  function isNameCharacter(character) {
17
20
  return character !== undefined && /[A-Za-z0-9_:-]/u.test(character);
18
21
  }
19
22
  function startsCfTag(source, index) {
23
+ if (source[index] !== "<")
24
+ return false;
20
25
  const afterSlash = source[index + 1] === "/" ? index + 2 : index + 1;
21
26
  return source.slice(afterSlash, afterSlash + 2).toLowerCase() === "cf" && isNameCharacter(source[afterSlash + 2]);
22
27
  }
@@ -291,7 +296,22 @@ function processTag(context, token, stack, findings) {
291
296
  throw new ToolFailure("UNSUPPORTED_SYNTAX", `Paired tag "${token.name}" cannot be self-closing`, 3);
292
297
  if (stack.length >= context.max_nesting)
293
298
  throw new ToolFailure("LIMIT_EXCEEDED", "The tag nesting limit was reached; no partial success is returned", 3);
294
- stack.push({ name: token.name, start: token.start, end: token.end, branch_else_seen: false });
299
+ if (token.name === "cfcatch" || token.name === "cffinally") {
300
+ const current = stack[stack.length - 1];
301
+ if (current?.name !== "cftry") {
302
+ addFinding(context, findings, finding(context, "INVALID_BRANCH", `${token.name} must belong to the directly open cftry`, token.start, token.end));
303
+ }
304
+ else {
305
+ if (current.try_finally_seen) {
306
+ const message = token.name === "cffinally" ? "cftry cannot contain more than one cffinally" : "cfcatch cannot appear after cffinally";
307
+ addFinding(context, findings, finding(context, "INVALID_BRANCH", message, token.start, token.end, current));
308
+ }
309
+ current.try_handler_seen = true;
310
+ if (token.name === "cffinally")
311
+ current.try_finally_seen = true;
312
+ }
313
+ }
314
+ stack.push({ name: token.name, start: token.start, end: token.end, branch_else_seen: false, try_handler_seen: false, try_finally_seen: false });
295
315
  return;
296
316
  }
297
317
  if (token.name === "cfscript" && stack[stack.length - 1]?.name !== "cfscript") {
@@ -310,6 +330,9 @@ function processTag(context, token, stack, findings) {
310
330
  addFinding(context, findings, finding(context, "MISMATCHED_CLOSE", `Closing tag "${token.name}" does not match open "${current.name}"`, token.start, token.end, current));
311
331
  return;
312
332
  }
333
+ if (current.name === "cftry" && !current.try_handler_seen) {
334
+ addFinding(context, findings, finding(context, "INVALID_BRANCH", "cftry must contain a directly nested cfcatch or cffinally", token.start, token.end, current));
335
+ }
313
336
  stack.pop();
314
337
  }
315
338
  export function scanCfml(source, limits) {
@@ -28,5 +28,9 @@ export function normalizeLimits(requested) {
28
28
  }
29
29
  values[key] = value;
30
30
  }
31
+ // Reserve enough space for the compact failure envelope at every hard limit.
32
+ if (values.max_output_bytes < 512) {
33
+ throw new ToolFailure("INVALID_LIMIT", "max_output_bytes must be at least 512", 2);
34
+ }
31
35
  return values;
32
36
  }
@@ -1,10 +1,14 @@
1
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
1
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { TextDecoder } from "node:util";
4
4
  import { ToolFailure } from "../schema/errors.js";
5
5
  function escapesRoot(root, candidate) {
6
6
  const relative = path.relative(root, candidate);
7
- return relative.startsWith("..") || path.isAbsolute(relative);
7
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
8
+ }
9
+ function sameFileState(left, right) {
10
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
11
+ left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
8
12
  }
9
13
  export function loadSource(options) {
10
14
  if (!/\.(?:cfm|cfc)$/iu.test(options.file)) {
@@ -32,19 +36,55 @@ export function loadSource(options) {
32
36
  if (escapesRoot(realRoot, resolvedPath)) {
33
37
  throw new ToolFailure("FILE_OUTSIDE_ROOT", "The selected file resolves outside the explicit root", 4);
34
38
  }
35
- const before = statSync(resolvedPath);
39
+ const before = statSync(resolvedPath, { bigint: true });
36
40
  if (!before.isFile())
37
41
  throw new ToolFailure("FILE_NOT_FOUND", "The selected path is not a regular file", 2);
38
- if (before.size > options.limits.max_source_bytes) {
42
+ if (before.size > BigInt(options.limits.max_source_bytes)) {
39
43
  throw new ToolFailure("LIMIT_EXCEEDED", `Source exceeds the ${options.limits.max_source_bytes}-byte limit`, 3);
40
44
  }
41
- const bytes = readFileSync(resolvedPath);
42
- const after = statSync(resolvedPath);
43
- if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
44
- throw new ToolFailure("SOURCE_CHANGED", "The source changed while it was being read", 3);
45
+ let descriptor;
46
+ let bytes;
47
+ const changed = () => new ToolFailure("SOURCE_CHANGED", "The admitted source changed before or during reading", 3);
48
+ const exceeded = () => new ToolFailure("LIMIT_EXCEEDED", "Source exceeds the configured byte limit", 3);
49
+ try {
50
+ // Admission must describe the descriptor we read, including ancestor replacements.
51
+ descriptor = openSync(resolvedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
52
+ const opened = fstatSync(descriptor, { bigint: true });
53
+ if (!opened.isFile())
54
+ throw changed();
55
+ if (opened.size > BigInt(options.limits.max_source_bytes))
56
+ throw exceeded();
57
+ const current = lstatSync(resolvedPath, { bigint: true });
58
+ if (!current.isFile() || !sameFileState(before, opened) || !sameFileState(opened, current) ||
59
+ realpathSync(resolvedPath) !== resolvedPath)
60
+ throw changed();
61
+ const buffer = Buffer.alloc(Number(opened.size) + 1);
62
+ let length = 0;
63
+ while (length < buffer.length) {
64
+ const count = readSync(descriptor, buffer, length, buffer.length - length, length);
65
+ if (count === 0)
66
+ break;
67
+ length += count;
68
+ if (length > options.limits.max_source_bytes)
69
+ throw exceeded();
70
+ }
71
+ const after = fstatSync(descriptor, { bigint: true });
72
+ if (after.size > BigInt(options.limits.max_source_bytes))
73
+ throw exceeded();
74
+ if (!sameFileState(opened, after) || after.size !== BigInt(length) ||
75
+ !sameFileState(after, lstatSync(resolvedPath, { bigint: true })) ||
76
+ realpathSync(resolvedPath) !== resolvedPath)
77
+ throw changed();
78
+ bytes = buffer.subarray(0, length);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof ToolFailure)
82
+ throw error;
83
+ throw changed();
45
84
  }
46
- if (bytes.byteLength > options.limits.max_source_bytes) {
47
- throw new ToolFailure("LIMIT_EXCEEDED", `Source exceeds the ${options.limits.max_source_bytes}-byte limit`, 3);
85
+ finally {
86
+ if (descriptor !== undefined)
87
+ closeSync(descriptor);
48
88
  }
49
89
  if (bytes.includes(0))
50
90
  throw new ToolFailure("BINARY_INPUT", "Binary or NUL-containing input is not supported", 2);
@@ -1,5 +1,5 @@
1
1
  export declare const TOOL_ID: "agent-cfml-check";
2
- export declare const TOOL_VERSION: "0.1.1";
2
+ export declare const TOOL_VERSION: "0.2.0";
3
3
  export declare const PROFILE_ID: "cfml-structure-v1";
4
4
  export interface Position {
5
5
  line: number;
@@ -1,3 +1,3 @@
1
1
  export const TOOL_ID = "agent-cfml-check";
2
- export const TOOL_VERSION = "0.1.1";
2
+ export const TOOL_VERSION = "0.2.0";
3
3
  export const PROFILE_ID = "cfml-structure-v1";
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "agent-cfml-check",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Deterministic, read-only structural checks for a bounded CFML subset.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/yapweijun1996/AI-Agent-Tools.git",
8
+ "directory": "packages/cfml-check"
9
+ },
10
+ "homepage": "https://github.com/yapweijun1996/AI-Agent-Tools/tree/main/packages/cfml-check#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/yapweijun1996/AI-Agent-Tools/issues"
13
+ },
5
14
  "type": "module",
6
15
  "license": "MIT",
7
16
  "main": "./dist/index.js",
@@ -28,9 +37,14 @@
28
37
  "scripts": {
29
38
  "typecheck": "tsc -p tsconfig.json --noEmit",
30
39
  "build": "tsc -p tsconfig.build.json",
31
- "test": "npm run build && tsc -p tsconfig.test.json && node --test .test-dist/test/check.test.js .test-dist/test/cli.test.js .test-dist/test/package.test.js .test-dist/test/schema.test.js",
40
+ "test": "npm run build && npm run test:prepared",
41
+ "test:prepared": "tsc -p tsconfig.test.json && node --test .test-dist/test/check.test.js .test-dist/test/cli.test.js .test-dist/test/package.test.js .test-dist/test/schema.test.js",
32
42
  "prepack": "npm run build"
33
43
  },
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org/",
46
+ "access": "public"
47
+ },
34
48
  "devDependencies": {
35
49
  "@types/node": "^22.10.0",
36
50
  "ajv": "^8.20.0",
@@ -118,7 +118,7 @@
118
118
  "max_source_bytes": { "type": "integer", "minimum": 1 },
119
119
  "max_nesting": { "type": "integer", "minimum": 1 },
120
120
  "max_findings": { "type": "integer", "minimum": 1 },
121
- "max_output_bytes": { "type": "integer", "minimum": 1 },
121
+ "max_output_bytes": { "type": "integer", "minimum": 512 },
122
122
  "time_limit_ms": { "type": "integer", "minimum": 1 }
123
123
  },
124
124
  "additionalProperties": false