musubix6 0.1.1 → 0.1.3
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/.github/skills/lean-agentic-coding/SKILL.md +1 -0
- package/.github/skills/lean-sdd-tdd/SKILL.md +39 -0
- package/.github/skills/lean-sdd-tdd/references/config.md +3 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-eslint.mjs +50 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-native.mjs +85 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-profiles.test.mjs +610 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-python.mjs +133 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-python.py +108 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-rust.mjs +120 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-typescript.mjs +53 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-worker.mjs +19 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics.mjs +212 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics.test.mjs +505 -0
- package/.github/skills/lean-sdd-tdd/scripts/sdd.mjs +36 -14
- package/.github/skills/lean-sdd-tdd/scripts/sdd.test.mjs +112 -60
- package/.github/skills/lean-sdd-tdd/scripts/source-discovery.test.mjs +96 -0
- package/.github/skills/lean-sdd-tdd/scripts/spec-context.mjs +131 -0
- package/.github/skills/lean-sdd-tdd/scripts/spec-context.test.mjs +190 -0
- package/.github/skills/sdd-reverse-spec/SKILL.md +1 -1
- package/.github/skills/sdd-spec-interview/SKILL.md +1 -1
- package/README-ja.md +352 -15
- package/README.md +332 -20
- package/package.json +6 -1
|
@@ -17,6 +17,7 @@ Rule: **rigor scales with risk, context scales with the diff.**
|
|
|
17
17
|
Unsure → pick the higher tier. Escalate mid-task if a T2 trigger appears; never downgrade silently.
|
|
18
18
|
|
|
19
19
|
## 1. Context budget (all tiers)
|
|
20
|
+
- With `.sdd/specs`, run the bundled `lean-sdd-tdd/scripts/sdd.mjs context <feature|REQ|file>...` before reading specs; `--changed` requires every changed file to resolve. Complete feature specs, artifacts and declared prerequisites are returned within 16384 UTF-8 bytes (`--max-bytes N` overrides explicitly). Resolve errors/overflow by narrowing selectors, not by bulk-reading all specs; add explicit selectors for relationships static impact cannot infer. Reading context never replaces approval/evidence or narrows full validation; run the full gate before merge.
|
|
20
21
|
- Read `AGENTS.md` (≤1 KB pointer file) and the single contract file only. Locate with grep/glob; `view` with line ranges; never read whole large files, lockfiles, coverage, `node_modules`, generated output.
|
|
21
22
|
- Run commands with quiet flags and `| tail`/`head`; report only failures + counts. Use `scripts/gate.sh` for compact results.
|
|
22
23
|
- Independent reads/searches in one parallel batch. Delegate to sub-agents only work needing separate context, and give them a bounded goal + stop condition + "return ≤200 words".
|
|
@@ -57,10 +57,49 @@ Batch: write all tests for a feature, then red each, implement, green each, **on
|
|
|
57
57
|
- Release qualification is separate from exploratory dogfooding: use the fixed support/candidate matrix and two-run evidence rules in `docs/design-sdd-hardening.md` §3.5 when present; otherwise use the release-readiness workflow in `references/enforced-rules.md`. Two arbitrary exploratory waves are not release proof, and technical readiness never replaces human release approval.
|
|
58
58
|
- Without Git, `gate --changed` explicitly falls back to full evidence/checks; manifest discovery, impact and C# stubs use filesystem discovery.
|
|
59
59
|
|
|
60
|
+
## 3b. Early project diagnostics
|
|
61
|
+
|
|
62
|
+
For an in-scope TS/JS project (root `tsconfig.json`, local TypeScript 5.9.x and
|
|
63
|
+
ESLint 9.x with one root flat config), run `$S diagnostics --root <app> --json`.
|
|
64
|
+
For standalone JS, run `$S diagnostics --root <app> --profile javascript --json`
|
|
65
|
+
(local ESLint 9.x and one root flat config, no TypeScript/tsconfig).
|
|
66
|
+
For Python, run `$S diagnostics --root <app> --profile python --json`
|
|
67
|
+
(root `.venv`: Python 3.14.x, Ruff 0.17.1 and mypy 2.4.0).
|
|
68
|
+
For Rust, run `$S diagnostics --root <app> --profile rust --json`
|
|
69
|
+
(installed cargo/rustc 1.99.x, root Cargo.toml/Cargo.lock, one package,
|
|
70
|
+
existing external Cargo home and offline dependencies).
|
|
71
|
+
Select a profile explicitly for those stacks; omission means TypeScript,
|
|
72
|
+
never autodetection. Run after each coherent code-edit batch and before claiming completion.
|
|
73
|
+
`PASS`/0 means all required providers completed without errors; `FAIL`/1 means static
|
|
74
|
+
errors were found; `INCOMPLETE`/2 means diagnosis could not finish. Report its
|
|
75
|
+
reasons, never call a missing tool or an ignored-only target set clean.
|
|
76
|
+
Repair only issues within the user's requested change scope; bound repair to
|
|
77
|
+
three attempts and disclose remaining issues instead of looping indefinitely.
|
|
78
|
+
Diagnostics are a snapshot, not Red, Green, approval or gate evidence.
|
|
79
|
+
Do not weaken or replace targeted tests, expected-assertion Red, approvals or
|
|
80
|
+
the final full gate because diagnostics passed. Never add diagnostics to gate
|
|
81
|
+
automatically. Never require TypeScript for standalone JS/Python/Rust, ESLint for
|
|
82
|
+
Python/Rust, or diagnostic tools for unsupported stacks.
|
|
83
|
+
Providers share a cumulative 8-MiB native stdout/stderr budget and have
|
|
84
|
+
60 seconds each, except cargo-check (180 seconds); worker output is separately capped.
|
|
85
|
+
Python uses isolated startup, root config/native exclusions, no bytecode/cache,
|
|
86
|
+
and null columns plus an info notice for mypy diagnostics on non-ASCII lines.
|
|
87
|
+
JS/Python enumeration fails closed if a symlink target outside excluded directories
|
|
88
|
+
cannot be resolved, even for an unrelated extension.
|
|
89
|
+
Rust checks all targets with default features, offline and locked. The parent owns
|
|
90
|
+
and removes private external target/build output after stopping workers; cleanup
|
|
91
|
+
failure is INCOMPLETE. Normal external Cargo cache bookkeeping/archive extraction
|
|
92
|
+
may persist; automatic cache GC is disabled. Do not promise sandboxing, network
|
|
93
|
+
isolation, or cleanup after parent SIGKILL/power loss/self-detached descendants.
|
|
94
|
+
The command does not install tools, fix sources or save evidence. Configurations,
|
|
95
|
+
plugins, build scripts and proc macros execute trusted local code with possible
|
|
96
|
+
arbitrary side effects. See both READMEs for selection/count/config details.
|
|
97
|
+
|
|
60
98
|
## 4. Review without humans
|
|
61
99
|
T2 only: parallel sub-agents by risk axis (security · correctness/state · contract · test adequacy), each diff-only, ≤200-word replies, findings to one file per feature, `.sdd/review-<feature>.md` (never share one `review.md` across features: it goes stale when the next feature overwrites it) (one line each: id|sev|path:line|state). Re-review Open items + fix diff only; stop after two clean rounds, then lock/merge.
|
|
62
100
|
|
|
63
101
|
## 5. Token rules
|
|
102
|
+
Before reading existing specs, run `$S context <feature|REQ|file>...` (or `--changed` when every changed file maps to specs). It returns complete selected specs, declared artifacts and recursive `plan.md` prerequisites, not a summary. Default stdout budget: 16384 UTF-8 bytes; override explicitly with `--max-bytes N`, use `--json` for automation. On errors/overflow, resolve selectors or narrow the task; never treat partial/missing context as sufficient, and never bulk-read all specs as a fallback. Static file impact cannot infer undeclared relationships: add explicit feature selectors when needed. Context is not approval/test evidence and does not narrow validation; keep the full gate before merge.
|
|
64
103
|
Read the spec and the specific files only; `view` ranges; quiet commands; one parallel batch for independent reads; no plan recaps; cite `path:line`. Suggest `/new` at boundaries. End with ≤8 lines: tier · REQs · gate verdict · open findings · next.
|
|
65
104
|
|
|
66
105
|
## Deterministic tests
|
|
@@ -21,9 +21,12 @@
|
|
|
21
21
|
- Tracker r32 (#348, #352): check-definition snapshots include Cargo workspace/package manifests, feature/target admission, default/custom build scripts and local build helpers; TypeScript checks include the selected/default tsconfig, JSONC `extends` chains (including package configs), and project references. Definition drift retains strict/warn policy. Cargo test gates also require every annotated Rust test in the checked surface to appear as executed, regardless of scope policy; direct commands request JSON artifact messages to distinguish same-named targets. Wrappers must preserve observable test output (and JSON artifact messages for ambiguous targets); quiet/hidden, ignored or zero-execution results cannot prove coverage.
|
|
22
22
|
- Tracker r32 (#349–#351): check provenance fingerprints PATH-selected executables and symlink targets, including ignored launchers (compared with Green rather than Git absence). The definition closure includes `.npmrc`, protected npm lifecycle wrappers, explicit `make -f`/`--file` inputs and local pytest plugins. Direct pytest runs use isolated, hook-free Python startup with explicit package directories; execution reports reject collection-only checks, missing annotated test identities and plugin-forged successful exit codes. Review definition changes and re-record with `tdd refactor`.
|
|
23
23
|
|
|
24
|
+
- Early diagnostics (development checkout, not published npm 0.1.2): `node <skill>/scripts/sdd.mjs diagnostics --root <app> --json [--profile typescript|javascript|python|rust]`. Default TypeScript uses root tsconfig, local TypeScript 5.9.x and ESLint 9.x with one root flat config. JavaScript uses that ESLint/config only and recursively selects JS/JSX/MJS/CJS (excluding .git/node_modules/.venv and directory symlinks). Python uses only root .venv (isolated Python 3.14.x, Ruff 0.17.1, mypy 2.4.0), root configs and native exclusions; direct .py/.pyi counts exclude .git/.venv/directory symlinks, not imported diagnostics. Config priority is Ruff .ruff.toml > ruff.toml > pyproject[tool.ruff], mypy mypy.ini > .mypy.ini > pyproject[tool.mypy] > setup.cfg[mypy]; no applicable config means isolated defaults. mypy non-ASCII columns are null with info/location-unavailable. Rust requires installed cargo/rustc 1.99.x, root manifest/lock, one package and existing external Cargo home/offline dependencies; all targets/default features run offline+locked. Both Rust build directories are parent-owned external scratch, removed after workers stop; cleanup failure is INCOMPLETE. Normal external Cargo cache bookkeeping/archive materialization persists, with automatic GC disabled. Rust counts distinct metadata target roots/observed roots, not every module. Exit 0/1/2 means PASS/FAIL/INCOMPLETE for the entire required provider set; warnings alone do not fail. Each provider cumulatively limits native stdout+stderr to 8 MiB and time to 60 seconds (Rust 180); worker protocol output has its own 8-MiB cap. This trusted-code snapshot does not install/fix/save history or replace Red, tests, approvals or the final gate, and is not injected into gate. Plugins/build scripts/macros are not sandboxed; network effects and cleanup after parent SIGKILL/power loss/self-detached descendants are not guaranteed. See SKILL section 3b and both READMEs.
|
|
24
25
|
- Install per-user (`~/.copilot/skills/lean-sdd-tdd`) when the target repo asserts its skill list (e.g. a test enumerating `.github/skills`); copying into `.github/skills` can break such tests. Run `node <skill>/scripts/sdd.mjs --root <repo> ...`.
|
|
25
26
|
- Build the target first (`npm run build`) if its CLI tests need build artifacts, before `gate`.
|
|
26
27
|
- `.sdd/config.json`: `scan:{include:[],exclude:["fixtures/**"]}` (globs; only comment-leading `@id` lines count; in Python, a line that starts a triple-quoted docstring or sits inside one also counts); `timeoutMs` globally or per check; check `changedCmd` with `{changedFiles}`/`{changedTests}` used by `gate --changed` (auto: `vitest related`, `jest --findRelatedTests`). `gate --changed` uses `changedTimeoutMs` (default 60s) so hub-file changes fail fast with `TIMEOUT`; narrow with `changedCmd` such as `["npx","vitest","run","{changedTests}","{changedScopes}"]` (scopes = package dirs). Always run the full `gate` (no `--changed`) before merge. Non-JS stacks get an auto `changedCmd` (no-pkg roots): Go `{changedGoPkgs}` (changed packages + reverse deps via `go list`), Rust `{changedCargoPkgs}` (`-p` changed crates + dependents via `cargo metadata`), Maven `{changedModulesCsv}` (changed modules + dependents + every upstream module they need, from the poms, so diamond dependencies resolve without `-am -amd`; falls back to the full run if the poms cannot be read), Go scoping also works in a repo without commits, Gradle `{changedGradleTasks}` (`:mod:cleanTest :mod:test` for changed modules plus dependents found from `project(':x')` references; not `buildDependents`, which misses later-evaluated dependents), CMake `{changedCtestRegex}` (`ctest -R` of changed test files). When a change cannot be scoped (go.mod/Cargo.*/root Gradle files, non-test C/C++ sources, CMake files, files outside any module) the full check runs and `! cmd …: cannot scope changes` is printed (for Rust: `cannot scope changes for the changed Cargo packages`). A `conftest.py` that defines only non-autouse fixtures the test never names (and no `pytest_*` hooks) is not an oracle of that test. `gate --changed` also scopes in tests whose recorded oracle files (e.g. a contract) changed. `impact` is file-granular (a note says so); the `tdd green` shared-implementation warning is per-`@implements`-block (ledger field `implBlocks`, #287; falls back to the whole file when the block cannot be delimited, and to file mtime for older ledger entries without the field). Hub detection (JS/TS relative-import graph): if > `hubThreshold` (0.25) of test files depend on the change, `hubFallbackCmd` runs (`{changedTests} {directTests}`); if that exceeds `hubMaxTests` (30) the check is skipped as INCOMPLETE instead of timing out. Workspace package names and tsconfig `paths` are followed. For hub changes the uncommitted diff is mapped to exported symbols (comment/whitespace-only ⇒ skipped; declarations mentioning the symbol are tainted transitively) so only tests that can reach them run; unknown diffs (non-exported/top-level code, `export *`) keep the whole hub set; an empty selection is INCOMPLETE, never an unscoped full run. Timeouts report `TIMEOUT`, not FAIL.
|
|
28
|
+
- Source discovery: `scan.maxFileBytes` is a positive safe integer (default `16777216`, 16 MiB). Supported source files are scanned completely up to and including the limit. Larger included sources cause an explicit `REFUSED: source scan` (exit 1) before evidence writes, rather than disappearing from trace/gate/impact. Explicit include/exclude filters and ignored generated directories apply before the bound; non-source documents are unaffected. This is a per-file bound, not a cumulative memory or latency guarantee.
|
|
29
|
+
- Repository self-validation uses `.sdd/config.json` and `npm test`, shared with CI/publish checks. The full-suite check has the existing CI's 100-minute execution allowance; this does not alter the individual compiler fixtures' existing 15-second limits. The initial 30-minute repository check was insufficient and its interrupted run was not counted as a pass. Annotated Node tests need unique literal ID-bearing titles; parameterized scenarios remain inside their single owned test, with all case rows and assertions retained.
|
|
27
30
|
- `flaky: {retries?: 1, policy?: "fail"|"warn"}` (#303): a failed `test` check is re-run `retries` times (0 disables). A pass on re-run prints `cmd test FLAKY suspected …`: `fail` (default) keeps the gate FAIL, `warn` accepts it with the warning. Each flaky event is appended to `.sdd/tdd.jsonl` as `{type:"flaky",check,retry,policy,outcome}`. Retries reporting only skipped/todo/xfail tests, zero tests, or no assertions cannot recover the failure or append flaky evidence (#335); remaining retries may still recover with an executed pass. A consistent failure stays a plain failure. Write deterministic tests: no fixed `sleep`, injected clock/random, port 0, generous timeouts, isolate signal/kill tests.
|
|
28
31
|
- `prepare: {cmd, outputs?, inputs?, timeoutMs?}` in config (auto-set from the `build` script by `init`): `gate` runs it before checks and caches by input hash (`.sdd/prepare-cache.json`); skipped when inputs and `outputs` are unchanged. `gate --changed` snapshots changed paths before preparation, so a preparation command that normalizes a lockfile cannot remove its workspace members or transitive consumers from check scope.
|
|
29
32
|
- Gate input integrity (#342): both full and changed gates hash annotated files, specs/approval artifacts, the ledger, policy/config inputs, protected oracle data and known check definitions before running commands. Direct prepare/check wrapper files and their local imports use the same protected closure. Each prepare, check, scoped fallback and retry is checked immediately afterward; mutation, deletion or added specs/annotated files fails regardless of `scopePolicy`, and remaining commands are not run. Mutating preparation is not cached. Declared `prepare.outputs` can regenerate oracle/artifact outputs, but cannot exempt annotated files, specs, the ledger, policy inputs or command definitions. Ordinary generated build outputs and gate-owned flaky ledger appends remain supported. These are command-boundary integrity checks, not a sandbox: a command that mutates and restores an input entirely within its execution is not detected.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { diagnostic, providerFailure, providerResult, sortDiagnostics } from './diagnostics.mjs';
|
|
4
|
+
import { loadLocal, selectTypeScript } from './diagnostics-typescript.mjs';
|
|
5
|
+
import { sourceInputs } from './diagnostics-native.mjs';
|
|
6
|
+
|
|
7
|
+
/** @id CODE-DIAGESL-001 @implements REQ-DIAGESL-001 REQ-DIAGESL-002 REQ-DIAGESL-003 REQ-DIAGESL-004 */
|
|
8
|
+
/** @id CODE-DIAGJS-001 @implements REQ-DIAGJS-001 REQ-DIAGJS-002 REQ-DIAGJS-003 */
|
|
9
|
+
export async function diagnoseESLint(root, { profile = 'typescript' } = {}) {
|
|
10
|
+
let output = providerResult('eslint');
|
|
11
|
+
try {
|
|
12
|
+
const { api: { ESLint }, version } = loadLocal(root, 'eslint', /^9\.\d+\.\d+$/);
|
|
13
|
+
output.version = version;
|
|
14
|
+
const configs = ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'].map(name => path.join(root, name)).filter(file => fs.existsSync(file));
|
|
15
|
+
if (configs.length !== 1) return providerFailure('eslint', 'Exactly one root ESLint flat config is required', output);
|
|
16
|
+
output.configPath = configs[0];
|
|
17
|
+
let inputs;
|
|
18
|
+
if (profile === 'javascript') inputs = sourceInputs(root, /\.(?:js|jsx|mjs|cjs)$/i, ['.git', 'node_modules', '.venv']).files;
|
|
19
|
+
else {
|
|
20
|
+
const { config, errors } = selectTypeScript(root);
|
|
21
|
+
if (!config || errors.length) return providerFailure('eslint', 'Invalid TypeScript config for ESLint input selection', output);
|
|
22
|
+
inputs = config.fileNames.filter(file => /\.(?:[cm]?[jt]s|[jt]sx)$/i.test(file) && !/\.d\.(?:[cm]?ts)$/i.test(file));
|
|
23
|
+
}
|
|
24
|
+
if (!inputs.length) return providerFailure('eslint', 'ESLint has no eligible input files', output);
|
|
25
|
+
const eslint = new ESLint({ cwd: root, overrideConfigFile: output.configPath, fix: false, cache: false, errorOnUnmatchedPattern: true });
|
|
26
|
+
let ignored = 0;
|
|
27
|
+
const targets = [];
|
|
28
|
+
for (const file of inputs) {
|
|
29
|
+
if (await eslint.isPathIgnored(file)) ignored++;
|
|
30
|
+
else targets.push(file);
|
|
31
|
+
}
|
|
32
|
+
if (!targets.length) return providerFailure('eslint', 'All ESLint input files are ignored or unconfigured', output);
|
|
33
|
+
for (const file of targets) {
|
|
34
|
+
const results = await eslint.lintFiles([file]);
|
|
35
|
+
if (results.length !== 1 || path.resolve(results[0].filePath) !== path.resolve(file)) throw new Error(`Unexpected ESLint results for ${file}`);
|
|
36
|
+
for (const value of results[0].messages) output.diagnostics.push(diagnostic('eslint', root, {
|
|
37
|
+
file: results[0].filePath, line: value.line ?? null, column: value.column ?? null,
|
|
38
|
+
code: value.ruleId ?? (value.fatal ? 'ESLINT_PARSE' : 'ESLINT_CONFIG'),
|
|
39
|
+
severity: value.severity === 2 ? 'error' : 'warning', message: value.message,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
output.status = 'complete';
|
|
43
|
+
output.reason = null;
|
|
44
|
+
output.files = { included: inputs.length, checked: targets.length, ignored };
|
|
45
|
+
output.diagnostics = sortDiagnostics(output.diagnostics);
|
|
46
|
+
return output;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return providerFailure('eslint', `ESLint diagnosis failed: ${error.message}`, output);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import { TextDecoder } from 'node:util';
|
|
6
|
+
|
|
7
|
+
/** @id CODE-DIAGCLI-003 @implements REQ-DIAGCLI-004 */
|
|
8
|
+
export function nativeBudget({ timeoutMs = 60000, outputBytes = 8388608, clock = () => performance.now() } = {}) {
|
|
9
|
+
const started = clock();
|
|
10
|
+
let bytes = 0, failure = null;
|
|
11
|
+
return {
|
|
12
|
+
async run(command, args, { cwd, env = process.env, input } = {}) {
|
|
13
|
+
const remaining = timeoutMs - (clock() - started);
|
|
14
|
+
if (remaining <= 0) failure ??= `Native provider timed out after ${timeoutMs} ms`;
|
|
15
|
+
if (failure) return { code: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), failure };
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
const stdout = [], stderr = [];
|
|
18
|
+
const child = spawn(command, args, { cwd, env, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
19
|
+
const abort = reason => {
|
|
20
|
+
failure ??= reason;
|
|
21
|
+
child.kill('SIGKILL');
|
|
22
|
+
child.stdout.destroy();
|
|
23
|
+
child.stderr.destroy();
|
|
24
|
+
child.stdin.destroy();
|
|
25
|
+
};
|
|
26
|
+
const timer = setTimeout(() => abort(`Native provider timed out after ${timeoutMs} ms`), remaining);
|
|
27
|
+
const capture = (chunk, output) => {
|
|
28
|
+
const available = Math.max(0, outputBytes - bytes);
|
|
29
|
+
bytes += chunk.length;
|
|
30
|
+
if (available) output.push(chunk.subarray(0, available));
|
|
31
|
+
if (bytes > outputBytes) abort(`Native provider output exceeds ${outputBytes} bytes`);
|
|
32
|
+
};
|
|
33
|
+
child.stdout.on('data', chunk => capture(chunk, stdout));
|
|
34
|
+
child.stderr.on('data', chunk => capture(chunk, stderr));
|
|
35
|
+
child.stdin.on('error', error => { failure ??= `Native input failed: ${error.message}`; });
|
|
36
|
+
child.on('error', error => { failure ??= `Native spawn failed: ${error.message}`; });
|
|
37
|
+
child.on('close', (code, signal) => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (clock() - started >= timeoutMs) failure ??= `Native provider timed out after ${timeoutMs} ms`;
|
|
40
|
+
resolve({ code, signal, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), failure });
|
|
41
|
+
});
|
|
42
|
+
child.stdin.end(input);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function nativeText(buffer) {
|
|
48
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(buffer);
|
|
49
|
+
}
|
|
50
|
+
export function jsonLines(buffer) {
|
|
51
|
+
const records = [];
|
|
52
|
+
let start = 0;
|
|
53
|
+
try {
|
|
54
|
+
while (start < buffer.length) {
|
|
55
|
+
const newline = buffer.indexOf(10, start);
|
|
56
|
+
const end = newline === -1 ? buffer.length : newline;
|
|
57
|
+
const line = nativeText(buffer.subarray(start, end));
|
|
58
|
+
if (line.trim()) records.push(JSON.parse(line));
|
|
59
|
+
start = end + 1;
|
|
60
|
+
}
|
|
61
|
+
return { records, error: null };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { records, error: `Invalid native JSONL: ${error.message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function inside(root, file) {
|
|
67
|
+
const relative = path.relative(root, file);
|
|
68
|
+
return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
69
|
+
}
|
|
70
|
+
export function sourceInputs(root, extensions, excluded = ['.git', '.venv']) {
|
|
71
|
+
const files = [], symlinkDirectories = [];
|
|
72
|
+
function walk(directory) {
|
|
73
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
74
|
+
const file = path.join(directory, entry.name);
|
|
75
|
+
if (excluded.includes(entry.name)) continue;
|
|
76
|
+
const stat = entry.isSymbolicLink() ? fs.statSync(file) : entry;
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
if (entry.isSymbolicLink()) symlinkDirectories.push(path.relative(root, file).split(path.sep).join('/'));
|
|
79
|
+
else walk(file);
|
|
80
|
+
} else if (stat.isFile() && extensions.test(entry.name)) files.push(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
walk(root);
|
|
84
|
+
return { files, symlinkDirectories };
|
|
85
|
+
}
|