musubix6 0.1.2 → 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.
@@ -57,6 +57,44 @@ 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
 
@@ -21,6 +21,7 @@
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.
@@ -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
+ }