From 64c3b2cea64128d9a3dd9e3f83ec979478b2d70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 27 Oct 2026 11:13:29 -0300 Subject: [PATCH 1/3] Document normative process runner contracts --- .changelog/normative-interface-contracts.md | 6 + docs/api/index.rst | 1 + docs/process-contracts.rst | 116 ++++++++++++++++++ src/Process/ProcessRunner.php | 53 +++++++- src/Process/ResultProcessRunner.php | 19 ++- .../Fixtures/normative-contract.after.php.inc | 35 ++++++ .../normative-contract.before.php.inc | 35 ++++++ tests/Integration/RectorTest.php | 24 ++++ tests/Integration/StandardsTest.php | 2 +- 9 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 .changelog/normative-interface-contracts.md create mode 100644 docs/process-contracts.rst create mode 100644 tests/Fixtures/normative-contract.after.php.inc create mode 100644 tests/Fixtures/normative-contract.before.php.inc diff --git a/.changelog/normative-interface-contracts.md b/.changelog/normative-interface-contracts.md new file mode 100644 index 0000000..e816948 --- /dev/null +++ b/.changelog/normative-interface-contracts.md @@ -0,0 +1,6 @@ +--- +category: fixed +type: patch +--- + +Document the process interfaces' normative execution, output and failure contracts, explain capture/run/result selection, and preserve descriptive PHPDoc and inheritance markers through qualified native formatting and refactoring. diff --git a/docs/api/index.rst b/docs/api/index.rst index 8885569..e9da497 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -5,3 +5,4 @@ API and extensions :maxdepth: 1 configuration + ../process-contracts diff --git a/docs/process-contracts.rst b/docs/process-contracts.rst new file mode 100644 index 0000000..4239d30 --- /dev/null +++ b/docs/process-contracts.rst @@ -0,0 +1,116 @@ +Process transport contracts +=========================== + +``ProcessRunner`` separates successful output capture from diagnostic execution. +``ResultProcessRunner`` is an optional capability for callers that need both +channels and the native exit status. These interfaces do not select a formatter, +refactoring policy or application bootstrap. + +Normative language +------------------ + +Uppercase requirement words in these contracts follow +`RFC 2119 `__ and +`RFC 8174 `__: + +* ``MUST`` and ``MUST NOT`` express a requirement and a prohibition. +* ``SHOULD`` and ``SHOULD NOT`` allow justified exceptions after their + consequences have been assessed. +* ``MAY`` identifies optional behavior. + +Lowercase words keep their ordinary meaning. Use these terms sparingly for +interoperability and consequential behavior, rather than turning every sentence +into a requirement. A method summary explains its purpose; subsequent paragraphs +describe semantics that parameter and return types cannot convey. Parameter, +return and exception tags add useful details instead of replacing the summary. + +Choosing an operation +--------------------- + +The output-delivery column describes ``NativeProcessRunner``. The operation +purposes and return/failure distinctions follow the interface contracts; custom +overrides document their own delivery and exception details. + +=================== =========================== ============================= ============================= +Operation Return on completion Completed nonzero exit Output delivery +=================== =========================== ============================= ============================= +``capture`` Successful stdout bytes Raises an execution failure Buffered result; no live sink +``run`` Native integer exit status Remains a nonzero status Configured sink, when present +``captureResult`` ``ProcessResult`` Remains result data Separate stdout/stderr bytes +=================== =========================== ============================= ============================= + +Use ``capture`` for a Git NUL-delimited path list or a successful Rector JSON +inventory: a failed command must not be mistaken for valid output. The transport +does not trim or decode stdout. Stderr is not concatenated into that return value. + +Use ``run`` for syntax, formatter or semantic-tool diagnostics when the caller +needs the exit code and its configured output boundary. The native provider +connects the sink to Console output, preserving stderr when the output supports +it. A directly constructed ``NativeProcessRunner`` with no sink stays silent. +Chunks retain bytes, but no total order between stdout and stderr is promised. +Streaming to a sink does not imply bounded memory: Symfony also retains buffers. +Exceptions from a configured sink may propagate instead of producing a status. + +Use ``captureResult`` when nonzero statuses and both output channels are useful +data. It does not parse diagnostics or decide that an engine result is clean. +An injected ``ProcessRunner`` override is not necessarily a +``ResultProcessRunner``; check the capability before using it. + +.. code-block:: php + + $paths = $process->capture(['git', 'ls-files', '-z'], $directory); + $status = $process->run([PHP_BINARY, '-n', '-l', $file], $directory); + + if (!$process instanceof \FastForward\CodeStandards\Process\ResultProcessRunner) { + throw new \RuntimeException('This transport does not support result capture.'); + } + + $result = $process->captureResult([PHP_BINARY, '-n', '-l', $file], $directory); + // Interpret $result->exitCode, stdout and stderr according to the selected tool. + +These examples assume an explicitly supplied transport, existing working +directory and selected source file. They do not construct a container or execute +an application bootstrap. The result-capture capability is a transport foundation; +it does not itself provide structured ECS/Rector reports or a CLI JSON option. + +Shared execution boundary +-------------------------- + +The executable and arguments are passed as a literal vector. The interface does +not interpolate a shell command. A caller can deliberately select a shell or +interpreter as the executable; its own command language remains the caller's +responsibility. The directory identifies the requested child working directory; +the native transport sets it without changing the parent's working directory. + +Callers supply a positive finite timeout to all three operations. The native +transport rejects invalid bounds before launching the child; the interface's +input precondition does not prescribe every override's validation algorithm. +The defaults are 120 seconds for ``capture`` and 300 seconds for ``run`` and +``captureResult``. The native implementation raises +``InvalidArgumentException`` for invalid bounds and Symfony Process runtime +exceptions when execution cannot complete, including timeout failures. +``capture`` also raises a process failure for a completed nonzero exit; ``run`` +and ``captureResult`` retain such statuses. A missing executable can itself +produce a native failure status; do not infer a particular exception from every +unsuccessful command. + +Capturing output is not a read-only guarantee. Any selected command or trusted +configuration may write files, execute code or contact external services. Callers +own that authority and the tool-specific interpretation of status and output. +The native transport removes ``GH_TOKEN``, ``GITHUB_TOKEN`` and ``COMPOSER_AUTH`` +from child environment inheritance. It does not promise an empty environment, +filesystem isolation, output redaction or rollback of child side effects. + +Preservation and extension +-------------------------- + +The default formatter is configured to retain descriptive interface PHPDoc, +normative clauses and inheritance markers. Golden fixtures exercise read-only +checking, explicit fixing and a stable second pass. A separately qualified native Rector rule also +preserves these contracts while transforming implementation code. Arbitrary +consumer-selected Rector rules can change PHPDoc; this is not a guarantee about +every semantic configuration. + +Custom transport overrides remain responsible for the documented execution and +failure semantics. They MAY implement only ``ProcessRunner``; the +``ResultProcessRunner`` capability is optional and callers discover it explicitly. diff --git a/src/Process/ProcessRunner.php b/src/Process/ProcessRunner.php index a4e7bda..de95633 100644 --- a/src/Process/ProcessRunner.php +++ b/src/Process/ProcessRunner.php @@ -4,18 +4,61 @@ namespace FastForward\CodeStandards\Process; -/** Executes bounded argument arrays without shell interpolation. */ +/** + * Executes bounded argument vectors without shell interpolation. + * + * Uppercase requirement terms use BCP 14 (RFC 2119 and RFC 8174). + * Callers own command authority and possible filesystem or application effects. + */ interface ProcessRunner { /** - * @param list $arguments Returns stdout or throws on failure. - * @param positive-float $timeout Finite maximum seconds for this subprocess; defaults retain Git's existing limit. + * Returns the standard-output bytes of a successfully completed subprocess. + * + * Callers MUST supply a complete executable/argument vector and a positive finite timeout. + * Implementations MUST NOT introduce shell interpolation into that vector. The directory + * identifies the requested child working directory. + * + * Failed execution MUST raise an execution failure rather than return output as a successful + * capture. Callers SHOULD use this operation when they need successful command output as data. + * Capturing output does not make the selected command read-only. + * + * NativeProcessRunner returns unmodified stdout without invoking its live output sink. It + * rejects invalid timeouts before launch and throws for a completed nonzero exit; its child + * working directory leaves the caller's directory unchanged. These describe the native + * implementation, not an additional output-sink or validation algorithm for every override. + * + * @param list $arguments Executable followed by literal arguments; callers supply the complete command. + * @param string $directory Requested working directory in which the command is to run. + * @param positive-float $timeout Finite running-time limit in seconds; this is not an unlimited mode. + * @return string Successful stdout; the native transport returns bytes without trimming or stderr concatenation. + * @throws \InvalidArgumentException In the native transport, when the timeout is nonfinite or not positive. + * @throws \Symfony\Component\Process\Exception\RuntimeException In the native transport, when execution cannot complete successfully. */ public function capture(array $arguments, string $directory, float $timeout = 120): string; /** - * @param list $arguments Streams diagnostics and returns the actual exit code. - * @param positive-float $timeout Finite maximum seconds for this subprocess; default formatting limit is unchanged. + * Runs a subprocess and returns its completed native exit status. + * + * The caller's argument, directory and timeout requirements of capture() also apply. + * Implementations MUST retain the completed native status, including nonzero findings or + * failures. They MUST NOT report successful completion when no completed status is available. + * + * Callers SHOULD select this operation for configured diagnostics and exit-status propagation. + * The selected command may change files; returning a status is not a read-only guarantee. + * + * NativeProcessRunner sends stdout/stderr chunks to its optional sink and stays silent without + * one. Its chunks retain bytes, but it does not establish total channel ordering or bounded + * memory. Native startup/timeout failures and exceptions from its sink can propagate instead + * of returning a status. Other overrides document their own output-delivery and exception details. + * + * @param list $arguments Executable followed by literal arguments; no shell command string is accepted. + * @param string $directory Requested child working directory; the native transport leaves the caller's unchanged. + * @param positive-float $timeout Finite running-time limit in seconds; zero never selects an unlimited mode. + * @return int Completed native exit status, including nonzero tool findings or failures. + * @throws \InvalidArgumentException In the native transport, when the timeout is nonfinite or not positive. + * @throws \Symfony\Component\Process\Exception\RuntimeException In the native transport, when execution cannot complete. + * @throws \Throwable In the native transport, when a configured sink fails while handling a chunk. */ public function run(array $arguments, string $directory, float $timeout = 300): int; } diff --git a/src/Process/ResultProcessRunner.php b/src/Process/ResultProcessRunner.php index f5c08c1..fd4c619 100644 --- a/src/Process/ResultProcessRunner.php +++ b/src/Process/ResultProcessRunner.php @@ -4,11 +4,26 @@ namespace FastForward\CodeStandards\Process; -/** Optional lossless capture capability; existing ProcessRunner implementations remain valid. */ +/** + * Captures separate output channels and status through an optional transport capability. + * + * Uppercase requirement terms use BCP 14 (RFC 2119 and RFC 8174). + * Callers MUST check this capability before using an arbitrary ProcessRunner override. + */ interface ResultProcessRunner extends ProcessRunner { /** - * Captures a completed process without streaming or throwing merely for a nonzero exit. + * Returns a completed subprocess's native status and separate output bytes. + * + * The caller's literal-argument, working-directory and finite-timeout requirements of ProcessRunner apply. + * Implementations MUST preserve stdout and stderr separately without trimming, decoding or + * concatenating them, including control bytes and invalid UTF-8. They MUST NOT send captured + * output to a live output sink. A completed nonzero status MUST remain result data, rather than + * cause an exception merely because it is nonzero. + * + * Failures that prevent completion MUST propagate; implementations MUST NOT fabricate a completed + * result for them. Callers SHOULD use this optional capability when output and nonzero statuses + * are both meaningful data. The result does not classify tool output or authorize source writes. * * @param list $arguments Literal executable and argument vector, without shell interpolation. * @param string $directory Explicit child working directory; the caller's directory remains unchanged. diff --git a/tests/Fixtures/normative-contract.after.php.inc b/tests/Fixtures/normative-contract.after.php.inc new file mode 100644 index 0000000..4bd3b8f --- /dev/null +++ b/tests/Fixtures/normative-contract.after.php.inc @@ -0,0 +1,35 @@ + Exactly one unchanged value, without normalization. + * @throws \InvalidArgumentException When the supplied value is negative. + */ + public function values(int $value): array; +} + +/** Supplies the inherited contract without duplicating its requirements. */ +final class NormativeImplementation implements NormativeContract +{ + /** {@inheritDoc} */ + public function values(int $value): array + { + if ($value < 0) { + throw new \InvalidArgumentException('Value must not be negative.'); + } + + return [$value]; + } +} diff --git a/tests/Fixtures/normative-contract.before.php.inc b/tests/Fixtures/normative-contract.before.php.inc new file mode 100644 index 0000000..b71f63c --- /dev/null +++ b/tests/Fixtures/normative-contract.before.php.inc @@ -0,0 +1,35 @@ + Exactly one unchanged value, without normalization. + * @throws \InvalidArgumentException When the supplied value is negative. + */ + public function values(int $value): array; +} + +/** Supplies the inherited contract without duplicating its requirements. */ +final class NormativeImplementation implements NormativeContract +{ + /** {@inheritDoc} */ + public function values(int $value): array + { + if ($value < 0) { + throw new \InvalidArgumentException('Value must not be negative.'); + } + + return array($value); + } +} diff --git a/tests/Integration/RectorTest.php b/tests/Integration/RectorTest.php index 85df401..6652b63 100644 --- a/tests/Integration/RectorTest.php +++ b/tests/Integration/RectorTest.php @@ -51,6 +51,30 @@ public function testPreviewApplyAndSecondPassWithExplicitNativeConfiguration(): self::assertSame($config, file_get_contents($this->directory . '/policy.php')); } + /** A qualified native semantic rule preserves descriptive interface contracts and inheritance markers. */ + public function testNativeArrayRulePreservesNormativeInterfaceDocumentation(): void + { + $fixtures = dirname(__DIR__) . '/Fixtures/'; + $source = file_get_contents($fixtures . 'normative-contract.before.php.inc'); + $expected = file_get_contents($fixtures . 'normative-contract.after.php.inc'); + self::assertIsString($source); + self::assertIsString($expected); + file_put_contents($this->directory . '/source.php', $source); + $this->configuration('\\Rector\\Php54\\Rector\\Array_\\LongArrayToShortArrayRector', 70400); + $before = $this->ownedBytes(); + + $preview = $this->cli(); + self::assertSame(2, $preview->getExitCode(), $preview->getOutput() . $preview->getErrorOutput()); + self::assertSame($before, $this->ownedBytes()); + $apply = $this->cli(['--apply']); + self::assertSame(0, $apply->getExitCode(), $apply->getOutput() . $apply->getErrorOutput()); + self::assertSame($expected, file_get_contents($this->directory . '/source.php')); + $after = $this->ownedBytes(); + self::assertSame(0, $this->cli()->getExitCode()); + self::assertSame(0, $this->cli(['--apply'])->getExitCode()); + self::assertSame($after, $this->ownedBytes()); + } + /** Empty native custom config is rejected before Rector's successful-but-writing initializer. */ public function testEmptyConfigDoesNotGenerateRectorFile(): void { diff --git a/tests/Integration/StandardsTest.php b/tests/Integration/StandardsTest.php index 176caec..ba4c308 100644 --- a/tests/Integration/StandardsTest.php +++ b/tests/Integration/StandardsTest.php @@ -365,7 +365,7 @@ public function testGoldenPresetIsReadOnlyUntilFixAndPreservesContracts(): void $fixtures = dirname(__DIR__) . '/Fixtures/'; $original = []; - foreach (['style', 'preservation', 'documentation'] as $name) { + foreach (['style', 'preservation', 'documentation', 'normative-contract'] as $name) { $original[$name] = file_get_contents($fixtures . $name . '.before.php.inc'); $this->put($name . '.php', $original[$name]); } From 0a251134764db4898d77d9becbf66514fba45b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 27 Oct 2026 11:46:26 -0300 Subject: [PATCH 2/3] Keep process contracts concise and implementation independent --- docs/process-contracts.rst | 125 ++++++------------ src/Process/ProcessRunner.php | 60 +++------ src/Process/ResultProcessRunner.php | 31 ++--- .../Fixtures/normative-contract.after.php.inc | 1 - .../normative-contract.before.php.inc | 1 - tests/Integration/RectorTest.php | 2 +- 6 files changed, 66 insertions(+), 154 deletions(-) diff --git a/docs/process-contracts.rst b/docs/process-contracts.rst index 4239d30..c234a64 100644 --- a/docs/process-contracts.rst +++ b/docs/process-contracts.rst @@ -1,60 +1,33 @@ Process transport contracts =========================== -``ProcessRunner`` separates successful output capture from diagnostic execution. -``ResultProcessRunner`` is an optional capability for callers that need both -channels and the native exit status. These interfaces do not select a formatter, -refactoring policy or application bootstrap. +``ProcessRunner`` distinguishes successful output capture from command status. +``ResultProcessRunner`` optionally adds separate raw output channels and status. Normative language ------------------ -Uppercase requirement words in these contracts follow +Uppercase ``MUST``, ``MUST NOT``, ``SHOULD``, ``SHOULD NOT`` and ``MAY`` follow `RFC 2119 `__ and -`RFC 8174 `__: - -* ``MUST`` and ``MUST NOT`` express a requirement and a prohibition. -* ``SHOULD`` and ``SHOULD NOT`` allow justified exceptions after their - consequences have been assessed. -* ``MAY`` identifies optional behavior. - -Lowercase words keep their ordinary meaning. Use these terms sparingly for -interoperability and consequential behavior, rather than turning every sentence -into a requirement. A method summary explains its purpose; subsequent paragraphs -describe semantics that parameter and return types cannot convey. Parameter, -return and exception tags add useful details instead of replacing the summary. +`RFC 8174 `__; lowercase words retain +their ordinary meaning. Use requirement terms sparingly for observable contracts. +Interface descriptions state semantic obligations, not implementation details. Choosing an operation --------------------- -The output-delivery column describes ``NativeProcessRunner``. The operation -purposes and return/failure distinctions follow the interface contracts; custom -overrides document their own delivery and exception details. - -=================== =========================== ============================= ============================= -Operation Return on completion Completed nonzero exit Output delivery -=================== =========================== ============================= ============================= -``capture`` Successful stdout bytes Raises an execution failure Buffered result; no live sink -``run`` Native integer exit status Remains a nonzero status Configured sink, when present -``captureResult`` ``ProcessResult`` Remains result data Separate stdout/stderr bytes -=================== =========================== ============================= ============================= - -Use ``capture`` for a Git NUL-delimited path list or a successful Rector JSON -inventory: a failed command must not be mistaken for valid output. The transport -does not trim or decode stdout. Stderr is not concatenated into that return value. - -Use ``run`` for syntax, formatter or semantic-tool diagnostics when the caller -needs the exit code and its configured output boundary. The native provider -connects the sink to Console output, preserving stderr when the output supports -it. A directly constructed ``NativeProcessRunner`` with no sink stays silent. -Chunks retain bytes, but no total order between stdout and stderr is promised. -Streaming to a sink does not imply bounded memory: Symfony also retains buffers. -Exceptions from a configured sink may propagate instead of producing a status. - -Use ``captureResult`` when nonzero statuses and both output channels are useful -data. It does not parse diagnostics or decide that an engine result is clean. -An injected ``ProcessRunner`` override is not necessarily a -``ResultProcessRunner``; check the capability before using it. +=================== =========================== ============================= +Operation Return value Completed nonzero exit +=================== =========================== ============================= +``capture`` Successful stdout Raises an exception +``run`` Completed exit status Returned unchanged +``captureResult`` Status, stdout and stderr Returned as data +=================== =========================== ============================= + +Use ``capture`` for output consumed as data, such as a Git path list or Rector +JSON inventory. Use ``run`` to propagate tool status, with diagnostics permitted. +Use ``captureResult`` for raw stdout/stderr and status without streaming; a +nonzero status remains data. Failure before completion raises an exception. .. code-block:: php @@ -68,49 +41,31 @@ An injected ``ProcessRunner`` override is not necessarily a $result = $process->captureResult([PHP_BINARY, '-n', '-l', $file], $directory); // Interpret $result->exitCode, stdout and stderr according to the selected tool. -These examples assume an explicitly supplied transport, existing working -directory and selected source file. They do not construct a container or execute -an application bootstrap. The result-capture capability is a transport foundation; -it does not itself provide structured ECS/Rector reports or a CLI JSON option. +The examples assume an injected transport, existing working directory and +selected source file. Check the optional capability before using an arbitrary +``ProcessRunner`` override. Result capture does not parse ECS/Rector reports. -Shared execution boundary --------------------------- +Native transport +---------------- -The executable and arguments are passed as a literal vector. The interface does -not interpolate a shell command. A caller can deliberately select a shell or -interpreter as the executable; its own command language remains the caller's -responsibility. The directory identifies the requested child working directory; -the native transport sets it without changing the parent's working directory. - -Callers supply a positive finite timeout to all three operations. The native -transport rejects invalid bounds before launching the child; the interface's -input precondition does not prescribe every override's validation algorithm. -The defaults are 120 seconds for ``capture`` and 300 seconds for ``run`` and -``captureResult``. The native implementation raises -``InvalidArgumentException`` for invalid bounds and Symfony Process runtime -exceptions when execution cannot complete, including timeout failures. -``capture`` also raises a process failure for a completed nonzero exit; ``run`` -and ``captureResult`` retain such statuses. A missing executable can itself -produce a native failure status; do not infer a particular exception from every -unsuccessful command. - -Capturing output is not a read-only guarantee. Any selected command or trusted -configuration may write files, execute code or contact external services. Callers -own that authority and the tool-specific interpretation of status and output. -The native transport removes ``GH_TOKEN``, ``GITHUB_TOKEN`` and ``COMPOSER_AUTH`` -from child environment inheritance. It does not promise an empty environment, -filesystem isolation, output redaction or rollback of child side effects. +``NativeProcessRunner`` uses Symfony Process. It rejects invalid timeouts before +launch and keeps the parent's working directory unchanged. Declared defaults +are 120 seconds for ``capture`` and 300 for ``run``/``captureResult``. + +Its ``run`` forwards raw stdout/stderr chunks to the optional sink; without one +it is silent. Capture operations do not use the sink. Symfony retains buffers, +so sink delivery is not a bounded-memory guarantee. Execution and sink exceptions +can propagate. + +The native child environment excludes ``GH_TOKEN``, ``GITHUB_TOKEN`` and +``COMPOSER_AUTH``. This is not a sandbox or an empty environment. Callers own +command authority: an explicitly selected shell, application or configuration +can execute code and change files. Capturing output does not make it read-only. Preservation and extension -------------------------- -The default formatter is configured to retain descriptive interface PHPDoc, -normative clauses and inheritance markers. Golden fixtures exercise read-only -checking, explicit fixing and a stable second pass. A separately qualified native Rector rule also -preserves these contracts while transforming implementation code. Arbitrary -consumer-selected Rector rules can change PHPDoc; this is not a guarantee about -every semantic configuration. - -Custom transport overrides remain responsible for the documented execution and -failure semantics. They MAY implement only ``ProcessRunner``; the -``ResultProcessRunner`` capability is optional and callers discover it explicitly. +Golden fixtures exercise descriptive contracts and inheritance markers through +read-only checking, explicit fixing and idempotence. A native Rector regression +changes array syntax while retaining the same documentation. Consumer-selected +semantic rules can change PHPDoc; this is not a claim about every configuration. diff --git a/src/Process/ProcessRunner.php b/src/Process/ProcessRunner.php index de95633..e995192 100644 --- a/src/Process/ProcessRunner.php +++ b/src/Process/ProcessRunner.php @@ -4,61 +4,33 @@ namespace FastForward\CodeStandards\Process; -/** - * Executes bounded argument vectors without shell interpolation. - * - * Uppercase requirement terms use BCP 14 (RFC 2119 and RFC 8174). - * Callers own command authority and possible filesystem or application effects. - */ +/** Executes bounded commands with literal arguments. */ interface ProcessRunner { /** - * Returns the standard-output bytes of a successfully completed subprocess. + * Captures successful standard output. * - * Callers MUST supply a complete executable/argument vector and a positive finite timeout. - * Implementations MUST NOT introduce shell interpolation into that vector. The directory - * identifies the requested child working directory. + * A nonzero exit MUST raise an exception rather than return successful output. * - * Failed execution MUST raise an execution failure rather than return output as a successful - * capture. Callers SHOULD use this operation when they need successful command output as data. - * Capturing output does not make the selected command read-only. - * - * NativeProcessRunner returns unmodified stdout without invoking its live output sink. It - * rejects invalid timeouts before launch and throws for a completed nonzero exit; its child - * working directory leaves the caller's directory unchanged. These describe the native - * implementation, not an additional output-sink or validation algorithm for every override. - * - * @param list $arguments Executable followed by literal arguments; callers supply the complete command. - * @param string $directory Requested working directory in which the command is to run. - * @param positive-float $timeout Finite running-time limit in seconds; this is not an unlimited mode. - * @return string Successful stdout; the native transport returns bytes without trimming or stderr concatenation. - * @throws \InvalidArgumentException In the native transport, when the timeout is nonfinite or not positive. - * @throws \Symfony\Component\Process\Exception\RuntimeException In the native transport, when execution cannot complete successfully. + * @param list $arguments Executable followed by literal arguments, without shell interpolation. + * @param string $directory Child working directory. + * @param positive-float $timeout Finite execution limit in seconds. + * @return string Successful stdout, excluding stderr diagnostics. + * @throws \Throwable When execution cannot complete successfully. */ public function capture(array $arguments, string $directory, float $timeout = 120): string; /** - * Runs a subprocess and returns its completed native exit status. - * - * The caller's argument, directory and timeout requirements of capture() also apply. - * Implementations MUST retain the completed native status, including nonzero findings or - * failures. They MUST NOT report successful completion when no completed status is available. - * - * Callers SHOULD select this operation for configured diagnostics and exit-status propagation. - * The selected command may change files; returning a status is not a read-only guarantee. + * Runs a command and returns its exit status. * - * NativeProcessRunner sends stdout/stderr chunks to its optional sink and stays silent without - * one. Its chunks retain bytes, but it does not establish total channel ordering or bounded - * memory. Native startup/timeout failures and exceptions from its sink can propagate instead - * of returning a status. Other overrides document their own output-delivery and exception details. + * Completed statuses, including nonzero ones, MUST be returned unchanged. + * Diagnostics MAY be emitted; failure before completion MUST raise an exception. * - * @param list $arguments Executable followed by literal arguments; no shell command string is accepted. - * @param string $directory Requested child working directory; the native transport leaves the caller's unchanged. - * @param positive-float $timeout Finite running-time limit in seconds; zero never selects an unlimited mode. - * @return int Completed native exit status, including nonzero tool findings or failures. - * @throws \InvalidArgumentException In the native transport, when the timeout is nonfinite or not positive. - * @throws \Symfony\Component\Process\Exception\RuntimeException In the native transport, when execution cannot complete. - * @throws \Throwable In the native transport, when a configured sink fails while handling a chunk. + * @param list $arguments Executable followed by literal arguments, without shell interpolation. + * @param string $directory Child working directory. + * @param positive-float $timeout Finite execution limit in seconds. + * @return int Completed command status; interpreting tool-specific outcomes belongs to the caller. + * @throws \Throwable When execution cannot complete. */ public function run(array $arguments, string $directory, float $timeout = 300): int; } diff --git a/src/Process/ResultProcessRunner.php b/src/Process/ResultProcessRunner.php index fd4c619..a3c5a97 100644 --- a/src/Process/ResultProcessRunner.php +++ b/src/Process/ResultProcessRunner.php @@ -4,33 +4,20 @@ namespace FastForward\CodeStandards\Process; -/** - * Captures separate output channels and status through an optional transport capability. - * - * Uppercase requirement terms use BCP 14 (RFC 2119 and RFC 8174). - * Callers MUST check this capability before using an arbitrary ProcessRunner override. - */ +/** Adds optional capture of output and status as data. */ interface ResultProcessRunner extends ProcessRunner { /** - * Returns a completed subprocess's native status and separate output bytes. + * Captures output and status as data. * - * The caller's literal-argument, working-directory and finite-timeout requirements of ProcessRunner apply. - * Implementations MUST preserve stdout and stderr separately without trimming, decoding or - * concatenating them, including control bytes and invalid UTF-8. They MUST NOT send captured - * output to a live output sink. A completed nonzero status MUST remain result data, rather than - * cause an exception merely because it is nonzero. + * Stdout and stderr MUST remain separate, unmodified byte strings, without streaming. + * A completed nonzero status MUST remain data; failure before completion MUST raise an exception. * - * Failures that prevent completion MUST propagate; implementations MUST NOT fabricate a completed - * result for them. Callers SHOULD use this optional capability when output and nonzero statuses - * are both meaningful data. The result does not classify tool output or authorize source writes. - * - * @param list $arguments Literal executable and argument vector, without shell interpolation. - * @param string $directory Explicit child working directory; the caller's directory remains unchanged. - * @param positive-float $timeout Finite maximum seconds using the same transport bounds as run(). - * @return ProcessResult Exact native status and separate output bytes, without a clean/failure interpretation. - * @throws \InvalidArgumentException When the timeout is nonfinite or not positive. - * @throws \Symfony\Component\Process\Exception\RuntimeException When execution cannot complete, including startup or timeout failure. + * @param list $arguments Executable followed by literal arguments, without shell interpolation. + * @param string $directory Child working directory, leaving the caller's unchanged. + * @param positive-float $timeout Finite execution limit in seconds. + * @return ProcessResult Completed result, without interpreting diagnostics or classifying success. + * @throws \Throwable When execution cannot complete or the timeout is invalid. */ public function captureResult(array $arguments, string $directory, float $timeout = 300): ProcessResult; } diff --git a/tests/Fixtures/normative-contract.after.php.inc b/tests/Fixtures/normative-contract.after.php.inc index 4bd3b8f..52e31cc 100644 --- a/tests/Fixtures/normative-contract.after.php.inc +++ b/tests/Fixtures/normative-contract.after.php.inc @@ -11,7 +11,6 @@ interface NormativeContract * Returns the supplied value as a one-element list. * * Implementations MUST preserve the supplied value and MUST NOT accept negative input. - * Callers SHOULD validate user input before invocation. * * @param int $value Nonnegative value whose identity the result retains. * @return list Exactly one unchanged value, without normalization. diff --git a/tests/Fixtures/normative-contract.before.php.inc b/tests/Fixtures/normative-contract.before.php.inc index b71f63c..f41da12 100644 --- a/tests/Fixtures/normative-contract.before.php.inc +++ b/tests/Fixtures/normative-contract.before.php.inc @@ -11,7 +11,6 @@ interface NormativeContract * Returns the supplied value as a one-element list. * * Implementations MUST preserve the supplied value and MUST NOT accept negative input. - * Callers SHOULD validate user input before invocation. * * @param int $value Nonnegative value whose identity the result retains. * @return list Exactly one unchanged value, without normalization. diff --git a/tests/Integration/RectorTest.php b/tests/Integration/RectorTest.php index 6652b63..f6c92a1 100644 --- a/tests/Integration/RectorTest.php +++ b/tests/Integration/RectorTest.php @@ -51,7 +51,7 @@ public function testPreviewApplyAndSecondPassWithExplicitNativeConfiguration(): self::assertSame($config, file_get_contents($this->directory . '/policy.php')); } - /** A qualified native semantic rule preserves descriptive interface contracts and inheritance markers. */ + /** Preserves interface documentation during refactoring. */ public function testNativeArrayRulePreservesNormativeInterfaceDocumentation(): void { $fixtures = dirname(__DIR__) . '/Fixtures/'; From 7b9f813651b7d848bd7d70361cdd3c7e186b5c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 27 Oct 2026 13:22:30 -0300 Subject: [PATCH 3/3] fix: preserve timeout descriptions in generated API docs --- src/Process/ProcessRunner.php | 4 ++-- src/Process/ResultProcessRunner.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Process/ProcessRunner.php b/src/Process/ProcessRunner.php index e995192..47d80a2 100644 --- a/src/Process/ProcessRunner.php +++ b/src/Process/ProcessRunner.php @@ -14,7 +14,7 @@ interface ProcessRunner * * @param list $arguments Executable followed by literal arguments, without shell interpolation. * @param string $directory Child working directory. - * @param positive-float $timeout Finite execution limit in seconds. + * @param float $timeout Positive, finite execution limit in seconds. * @return string Successful stdout, excluding stderr diagnostics. * @throws \Throwable When execution cannot complete successfully. */ @@ -28,7 +28,7 @@ public function capture(array $arguments, string $directory, float $timeout = 12 * * @param list $arguments Executable followed by literal arguments, without shell interpolation. * @param string $directory Child working directory. - * @param positive-float $timeout Finite execution limit in seconds. + * @param float $timeout Positive, finite execution limit in seconds. * @return int Completed command status; interpreting tool-specific outcomes belongs to the caller. * @throws \Throwable When execution cannot complete. */ diff --git a/src/Process/ResultProcessRunner.php b/src/Process/ResultProcessRunner.php index a3c5a97..73ed336 100644 --- a/src/Process/ResultProcessRunner.php +++ b/src/Process/ResultProcessRunner.php @@ -15,7 +15,7 @@ interface ResultProcessRunner extends ProcessRunner * * @param list $arguments Executable followed by literal arguments, without shell interpolation. * @param string $directory Child working directory, leaving the caller's unchanged. - * @param positive-float $timeout Finite execution limit in seconds. + * @param float $timeout Positive, finite execution limit in seconds. * @return ProcessResult Completed result, without interpreting diagnostics or classifying success. * @throws \Throwable When execution cannot complete or the timeout is invalid. */