diff --git a/.env.example b/.env.example index abd4b706..cf75c6ef 100644 --- a/.env.example +++ b/.env.example @@ -33,7 +33,9 @@ CRONY_COPILOT_FIXTURE=false CRONY_RUNNER_GRACE_SECS=5 CRONY_RUNNER_CREDENTIAL_TTL_SECS=86400 CRONY_MODE=development -CRONY_SECRET_MASTER_KEY_HEX=a5c3f1458279dfb241239378dbefa6b8d2ab32703cba1768343712fd37ac1f04 +# Generate a unique 32-byte key outside source control and load it through trusted host configuration. +# Do not reuse a committed example value. If an old sample key was deployed, rotate it separately. +# CRONY_SECRET_MASTER_KEY_HEX= CRONY_OBJECT_STORE_BACKEND=local CRONY_OBJECT_STORE_LOCAL_ROOT=./output/artifact-objects CRONY_ARTIFACT_MAX_BYTES=16777216 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..4b507580 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Routing only: require-code-owner-review must be enabled separately in native rules. +* @All-The-Vibes/ecorp-team diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ca71894f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/skills/readiness-improvement/SKILL.md b/.github/skills/readiness-improvement/SKILL.md new file mode 100644 index 00000000..90bf4fc8 --- /dev/null +++ b/.github/skills/readiness-improvement/SKILL.md @@ -0,0 +1,39 @@ +--- +name: readiness-improvement +description: Improve repository readiness through evidence-backed local validation and the installed CodeBlend evaluator, without claiming hosted authority. +--- + +# Readiness improvement + +Use an isolated contribution worktree and read the root AGENTS.md contracts first. +Do not duplicate the shared review workflow proposed in PR #304. This skill only +defines the readiness measurement/change loop; it grants no tools or permissions. + +1. Resolve the current worktree, HEAD and dirty state. Synchronize the approved branch + before a batch, never during a benchmark. Preserve all unrelated work. +2. Use the separately installed `codeblend-ai-composite` skill and its native executable. + Evaluate the local path, not the remote URL. Record the evaluator digest, platform, + CLI compatibility, models, evidence window and source identity. Never edit scores. +3. Read actual findings, distinguish unsupported detection from missing engineering, + and propose one bounded change with an acceptance test. Keep native Node/Cargo test + semantics; never rename or add dummy tests to improve a filename heuristic. +4. Preview `pnpm check:preview`, implement the approved local scope, then run focused + regressions and `pnpm check`. Review machine-readable `output/readiness/` receipts. + Failed, cancelled, ignored and unexecuted tests remain separate from passes. +5. Re-run the same evaluator settings after a meaningful validated batch. Record every + attempted result, including regressions and errors, in `docs/AI_READINESS.md`. + +Bound each invocation to at most **three meaningful change/measurement cycles**. +Stop earlier after two cycles without evidence-backed progress, on cancellation, +ambiguous ownership, a safety failure, or a required external permission. Summarize +remaining work for an explicitly authorized continuation; do not loop on random judge variance. +The user's larger objective is not proof that a mathematical maximum has been reached. + +Never enable live credentials, widen permissions, change branch rules, publish, merge, +or deploy as a score optimization. Document these dependencies for the responsible owner. +Installing a workflow is not evidence it ran, and local results do not establish +production identity, real-provider reliability or recurring hosted operation. + +Use the existing deterministic docs tool for its exact generated contract only: +`pnpm check:docs:preview`, review, `pnpm check:docs:write`, `pnpm check:docs`. +It does not repair arbitrary prose or make repository-wide semantic drift claims. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54035fc3..1907abd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,28 +11,34 @@ permissions: jobs: quality: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable action; compiler pinned below with: + toolchain: 1.98.1 components: rustfmt, clippy - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 11.19.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 - run: pnpm install --frozen-lockfile - - run: node tools/check_migrations.mjs - - name: Verify external-adapter E2E expectations cannot hide runtime failures - run: node --test tools/e2e_external_adapters.test.mjs tools/fixture_source_identity.test.mjs tools/owned_test_stack.test.mjs - - run: cargo fmt --all -- --check - - run: cargo clippy --workspace --all-targets -- -D warnings - - run: cargo test --workspace - - run: pnpm build:web - - run: pnpm lint:web + - name: Install the optional steward test SDK from its committed lockfile + run: npm ci --prefix scenarios/repo-steward --ignore-scripts --no-audit --no-fund + - name: Validate migrations, documentation drift, native tests, Rust and web + run: pnpm check + - name: Upload machine-readable validation report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: validation-report + path: output/readiness/*.json + if-no-files-found: warn + retention-days: 14 integration: runs-on: ubuntu-latest @@ -57,18 +63,20 @@ jobs: --health-timeout 3s --health-retries 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - - uses: dtolnay/rust-toolchain@stable - - uses: pnpm/action-setup@v4 + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable action; compiler pinned below + with: + toolchain: 1.98.1 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 11.19.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 - run: pnpm install --frozen-lockfile - run: cargo build -p crony-server -p crony-runner -p crony-cli -p crony-gateways - name: Start control and execution planes @@ -213,7 +221,7 @@ jobs: run: node tools/e2e_chaos_report.mjs - name: Upload runtime evidence if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: integration-evidence path: | @@ -266,15 +274,17 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable action; compiler pinned below + with: + toolchain: 1.98.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: 22 - - uses: Swatinem/rust-cache@v2 + node-version-file: .node-version + - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 - run: cargo test -p crony-runner - run: node tools/platform_runner_contract.mjs - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: runner-platform-${{ matrix.os }} path: output/platform/* @@ -282,16 +292,18 @@ jobs: desktop-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable action; compiler pinned below + with: + toolchain: 1.98.1 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 11.19.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 - run: pnpm install --frozen-lockfile - run: pnpm build:web - run: cargo install tauri-cli --version 2.11.4 --locked diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..7fe35e27 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,52 @@ +name: Security + +# CodeQL is managed by the repository's existing native default setup for Actions, +# JavaScript/TypeScript, Python and Rust. Do not add a conflicting advanced upload job. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + secrets: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Install checksum-pinned Gitleaks CLI + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gitleaks" + curl --fail --silent --show-error --location --proto '=https' \ + https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \ + -o "$RUNNER_TEMP/gitleaks/gitleaks.tar.gz" + echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb $RUNNER_TEMP/gitleaks/gitleaks.tar.gz" | sha256sum --check --strict + tar -xzf "$RUNNER_TEMP/gitleaks/gitleaks.tar.gz" -C "$RUNNER_TEMP/gitleaks" gitleaks + - name: Scan source snapshot for secrets (redacted) + shell: bash + run: | + mkdir -p output/security + "$RUNNER_TEMP/gitleaks/gitleaks" dir . --config .gitleaks.toml \ + --redact=100 --no-banner --report-format json --report-path output/security/gitleaks.json + - name: Prove detector rejection and redacted output using synthetic canary + run: node tools/security_canary.mjs "$RUNNER_TEMP/gitleaks/gitleaks" + - name: Retain redacted security report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: secret-scan-report + path: output/security/gitleaks.json + if-no-files-found: warn + retention-days: 7 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..8b3533c4 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,22 @@ +title = "ECorp source secret detection" +[extend] +useDefault = true + +[allowlist] +description = "Generated dependencies, build output, Git metadata and local evidence are not repository source" +paths = ['''(^|[\\/])(node_modules|target|target-audit|\.git|output)[\\/]'''] + +# These exact historical fixture fields identify replay operations, not credentials. +# Do not ignore entire evidence directories or API-key-shaped values elsewhere. +[[rules]] +id = "generic-api-key" +[[rules.allowlists]] +description = "Non-secret fixture/replay identifiers in three reviewed test artifacts" +condition = "AND" +paths = [ + '''^docs/evidence/2026-09-08-room-context-browser\.json$''', + '''^docs/evidence/assets/predispatch-failure/runtime\.json$''', + '''^tools/e2e_factory_run_activity\.py$''', +] +regexTarget = "match" +regexes = ['''(idempotency_key|fixture_key)["']?\s*:\s*["']'''] diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..60ade1ae --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.19.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..584c0f32 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +# Optional local feedback. CI executes the same checks without relying on hook installation. +repos: + - repo: local + hooks: + - id: documentation-contract + name: Check documented validation contract + entry: node tools/check_docs.mjs + language: system + pass_filenames: false + always_run: true + - id: rust-format + name: Check Rust formatting without editing files + entry: cargo fmt --check + language: system + pass_filenames: false + types: [rust] + - id: readiness-tool-tests + name: Check validation and evidence tooling + entry: node --test tools/readiness_contract.test.mjs + language: system + pass_filenames: false + files: '^(tools/(run_checks|check_docs|readiness_contract\.test)\.mjs|test\.config\.json)$' diff --git a/README.md b/README.md index 57d7a799..1f21414d 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,11 @@ pnpm check This runs migration checks, Rust formatting, Clippy, the workspace test suite, and the web build and lint. +It also runs the native Node regression suites and the documentation-contract drift gate. +Use `pnpm check:preview` to inspect the exact commands without executing them, or +`pnpm check:fast` for quick feedback. See [reproducible validation](docs/VALIDATION.md) +for pinned toolchains, test discovery, evidence receipts, and ignored-test boundaries. + ## Run a GitHub issue through the factory [ECorp Build, organization GitHub Project #5](https://github.com/orgs/All-The-Vibes/projects/5) is diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md new file mode 100644 index 00000000..d8d4e2ea --- /dev/null +++ b/apps/web/AGENTS.md @@ -0,0 +1,17 @@ +# Web contribution context + +The root AGENTS.md and product/security contracts still apply. + +- Start with the relevant pure projection/reader beside App.tsx, not the entire UI file. +- Run `pnpm test:js` from the repository root; Node 24 executes the existing `.test.mjs` + suites and their TypeScript imports. `pnpm build:web` type-checks and builds; + `pnpm lint:web` checks the web source. See `docs/VALIDATION.md` at the repo root. +- Server snapshots, exact context readers and persisted run IDs are authoritative. + Missing/denied/incomplete context stays unknown; do not substitute another run or viewer. +- Scope state by server, Corp, actor, room/mission and selected source where applicable. + A remembered run ID grants no authority. Comments are not approvals or steering commands. +- Keep mission, task, run, producer and verifier identities distinct in evidence views. +- For changed user-visible behavior, verify the browser-to-server-to-runner path in an owned + isolated stack, including reconnect and 390px layout. SSR/unit success is not that proof. +- Coordinate changes to App.tsx with active UI work; extract a bounded responsibility only + with regression coverage. Do not rewrite shared schemas or lockfiles incidentally. diff --git a/crates/AGENTS.md b/crates/AGENTS.md new file mode 100644 index 00000000..225b11bb --- /dev/null +++ b/crates/AGENTS.md @@ -0,0 +1,18 @@ +# Rust contribution context + +The root AGENTS.md and product/security contracts still apply. + +- Domain contains pure policies; protocol contains wire DTOs; store owns transactional + persistence; server owns authenticated control; runner owns isolated execution; CLI and + gateways adapt supported native operations. Preserve these boundaries. +- Start with the affected crate's Cargo.toml and relevant module. Use a focused package/test + filter first, then `cargo fmt --check`, workspace Clippy and workspace tests before committing. +- The compiler toolchain is pinned in rust-toolchain.toml; Cargo.lock remains authoritative. +- Inline tests and macro-generated cases are real Rust tests. Filename or annotation counts + are not execution counts. Report passed, failed and ignored results separately. +- SQLx tests marked ignored require explicitly owned disposable fixtures. Never inherit a + retained application's database just to run them. Native-session probes remain opt-in. +- Prefer native harness capabilities and existing recovery/approval operations over parallel + orchestration machinery. Scope admission by Corp, actor, run, source and current authority. +- Completion needs persisted verifier evidence. A code path, configured gate or successful + unit test does not establish real-provider, hosted or production acceptance. diff --git a/docs/AI_READINESS.md b/docs/AI_READINESS.md new file mode 100644 index 00000000..4df9d3fb --- /dev/null +++ b/docs/AI_READINESS.md @@ -0,0 +1,190 @@ +# AI-readiness improvement ledger + +Branch: `codex/ai-readiness-improvements`. Source base: `0b1ad59da398e3dbd6a696d0264bcb6ebd620219`. +This is local engineering/measurement work, not approval to push, merge, deploy or alter +GitHub policy. Hourly main synchronization is held during active changes and measurement. + +**Latest verified benchmark: 67.1 composite, 87.0 foundations, 51.75 operations.** +The fresh baseline was 54.2 / 66.8 / 44.0. AI-Ready remains **no**: both axes must be >=80. +All three judges returned valid reports in the final run. This is the best observed local +candidate in this pass, not a claim that the global maximum has been achieved. + +## Measurement protocol + +Use the installed `codeblend-ai-composite` skill against this local worktree, including +uncommitted changes when explicitly recorded. Preserve the evaluator's original artifacts. +Never adjust its score using our independent Rust/Node cross-checks. +The composite is the geometric mean of foundations and operations; AI-Ready requires both >=80. + +Pinned evaluator SHA-256: `8acdb9d786842516acd395b74263502285fe8232bb71addd62fe81e94a890633`. +Distribution: CodeBlend v0.1.24; its embedded `--version` prints 0.1.1, so identify it by hash. +Host: Windows ARM64, using the previously approved x64 compatibility exception. +Panel: `claude-opus-5`, `gpt-6-astra`, `claude-sonnet-5`; one round, 90-day API window, +fresh runs with API evidence required. The CLI's own stages generate all scores. + +The installed Copilot CLI 1.0.84-5 renamed `--effort` to `--reasoning-effort`; the evaluator +incorrectly describes the resulting failure as an old CLI. The benchmark uses an isolated +copy of the existing 1.0.80-0 binary, with documented `COPILOT_AUTO_UPDATE=false` and a +process-local PATH. The installed/default CLI and ECorp SDK/runtime configuration are unchanged. +Historical evaluations used 1.0.79, so disclose this runtime difference in comparisons. + +| Run (UTC+8 ID) | Source | Composite | Foundations | Operations | Status | +| --- | --- | ---: | ---: | ---: | --- | +| 20260918-121938+0800 | clean base | — | 66.8 | — | Failed: no judge consensus due to CLI flag incompatibility | +| 20260918-122201+0800 | clean base | 54.2 | 66.8 | 44.0 | Complete, three valid judges | +| iteration-1 substrate only | uncommitted local changes | — | 77.8 | — | Deterministic native scanner only, not a composite benchmark | +| 20260918-124151+0800 | uncommitted iteration 1 | 60.0 | 77.8 | 46.25 | Complete, three valid judges; files configured locally, hosted operation unproven | +| 20260918-124753+0800 | uncommitted iteration 2 | 67.1 | 87.0 | 51.75 | Complete, three valid judges; Q2 Verifiable-but-manual, not AI-Ready | +| 20260918-125756+0800 | validated candidate plus receipts | 67.1 | 87.0 | 51.75 | Final complete run, all seven stages executed, three valid judges | + +Original run artifacts live beneath the evaluator's per-repository `runs/` directory. +Local validation receipts live in ignored `output/readiness/`; share only reviewed, redacted +evidence, not raw credentials or arbitrary logs. A HEAD alone does not identify a dirty tree: +the driver records tracked-diff and untracked-file digests and rejects a changed source snapshot. + +## Implemented locally + +- Discoverable native Node/Cargo test entry points and a reviewed test-discovery configuration; + the existing suites are retained, not renamed or replaced with synthetic score fixtures. +- Migration, docs, Node, Rust formatting, Clippy, Rust tests and web build/lint form one gate; + failures stop it, with remaining checks explicitly not run. +- Node 24.19.0/Rust 1.98.1 pinning, optional local pre-commit checks and module guidance. +- A generated validation-command contract with a deterministic check and narrow explicit repair; + regression cases prove drift detection, prose preservation and ambiguous-marker rejection. +- Immutable CI Action references and structured validation artifacts. +- Gitleaks snapshot scanning with checksum-pinned CLI, redacted reports and a seeded negative + canary. A proposed JavaScript-only CodeQL job was removed in the publication review: + GitHub's existing native default setup already covers Actions, JavaScript/TypeScript, + Python and Rust. Its passing current-main checks were verified without changing that setup. +- Removed the fixed master-key value from `.env.example`; no deployed key or retained database + was changed. Three exact fixture/replay fields have reviewed secret-scan exceptions. +- Native Dependabot configuration and CODEOWNERS review routing. Neither is proof of hosted + execution or enforced required review on this unpushed branch. +- Declared the previously ambient `ws` test dependency. All other lockfile versions remain + at the source base. The separate optional Teams SDK uses its existing committed npm lock. + +## Validation findings retained + +The initial broad Node run found five failures: the native Teams test SDK and the `ws` +transport dependency were unavailable. A subsequent run with dependencies resolved passed +1026 cases but cancelled one 15-second cold-start Teams test while compilation was active; +it was not called a passing suite. That exact native Teams test then passed alone in about +one second, without timeout changes or weaker authentication. Final full-gate status is +recorded below only after a complete fresh run. + +Gitleaks initially reported four findings. The example master key was removed, not allowlisted. +Three non-secret fixture/idempotency fields received path-and-rule-scoped exceptions. The +follow-up snapshot scan passed, and an independent high-entropy synthetic credential canary +still caused rejection inside an otherwise allowlisted artifact, with report bytes redacted. + +## Blocked or deliberately deferred changes + +| Item | Why it is not silently changed | Required follow-up | +| --- | --- | --- | +| Rust and `.test.mjs` discovery | The evaluator still detects only a small subset despite native execution. Its CLI has no documented external-result import. | Provide an upstream Cargo/Node reproduction; use an officially supported evaluator update, not fake/renamed tests. | +| Required CI/security checks and code-owner review | Files do not establish native ruleset enforcement. | An authorized owner must review current check producers and apply the exact policy after hosted validation. | +| Hosted secret scan and dependency updates | New source configuration has not yet produced hosted receipts. Native CodeQL default setup is already configured and passing. | Publish the draft and retain new hosted receipts; preserve native CodeQL rather than adding a conflicting advanced job. | +| Automatic agent repair / recurring maintenance | Repo Steward is intentionally read-only. A schedule alone is not a verified closed loop. | Approve one bounded native-harness pilot with independent validation, budgets, cancellation, rollback and real recurring receipts. No auto-merge. | +| SQLx and native-session ignored tests | Need explicitly owned fixtures, not the user's retained office/database. | Run separately scoped acceptance; keep ignored counts separate. | +| Example key exposure in old history | Removing a sample from the current tree does not revoke any deployed use. | If anyone used it outside disposable development, owner must rotate through the secret-management process. Do not rewrite Git history automatically. | +| Clean dependency bootstrap on this host | npmjs.org TLS handshakes fail. The existing Microsoft public package mirror can serve dependencies, but strict pnpm tarball-URL checks reject mixing registry identities with the original lock. | Restore approved registry connectivity or agree a separately reviewed mirror/lock policy; never disable TLS or supply-chain checks. Current installed-dependency checks are not clean-install proof. | +| Real-provider/production acceptance | Local fixtures and unit suites cannot certify these boundaries. | Run separately authorized browser/server/runner and provider acceptance with genuine identities. | +| Runnable devcontainer | Docker CLI exists, but the Docker Desktop Linux engine pipe is unavailable on this host. A config-only addition is not a proven bootstrap. | Validate a scoped Linux development lane on an available daemon; preserve the Windows-only native-provider boundary. | +| Harness/MCP bootstrap configuration | No project-scoped authorized endpoint/identity was selected for automatic agent-tool startup. | Review native harness capabilities and exact endpoints; do not add placeholder tool grants solely for a documentation score. | + +The honest optimization target is the best validated result within the authorized scope, not +an invented global maximum. Do not remove working security or review boundaries for points. + +## Validated iteration 1 and further hardening + +The complete `pnpm check` receipt at `2026-09-18T04-41-42-248Z-full.json` passed all eight +checks with no source changes during validation: 1027 Node cases passed; 554 Rust cases +passed, zero failed and 343 were explicitly ignored. Migration validation covered 41 +migrations; formatting, Clippy, web build and web lint passed. The portable receipt is +[retained here](reports/ai-readiness-validation-iteration-1.json), with its exact earlier +source fingerprint. Later tooling/ledger edits are not retroactively covered by that receipt. + +Iteration 2 adds black-box tests of the actual CLI: mutation-free preview, argument rejection, +failure short-circuiting, observed result counts, and invalidation after a passing test edits +source. A nested-test environment failure was retained and fixed in the test fixture; the +driver now also rejects a zero-exit test command with no observed test summary. Windows +CRLF checkouts and native pnpm argument vectors have explicit regressions. These are new +functional tests of the new tooling, not renamed legacy tests or synthetic product evidence. + +The 14 focused helper/CLI tests and all four YAML configuration parses passed. At that +checkpoint the complete validation and benchmark were still pending; their final results +are recorded below. No user-facing application code or deployed policy was changed. + +The native scanner now recognizes five test files and scores Testing L4: the existing +native command/configuration plus the new black-box CLI suite satisfy its configured-suite +rule. This does not resolve the underlying undercount of inline Rust or `.test.mjs` cases. +Foundations reached 87.0, but operations remain 51.75. The original score is retained. + +The broad iteration-2 test invocation, run concurrently with the judge benchmark, again +cancelled the same 15-second Teams SDK startup case (1034 passed, zero failed, one cancelled). +This is a retained load-sensitive failure, not a green run. Final validation is serialized +without our benchmark processes; neither authentication checks nor timeouts are weakened. + +The remaining operation gap is explicit in the judges' findings: source files and a local +report do not establish recurring hosted repair, learned-rule promotion, accepted autonomous +changes or production evidence. Cleanup capability remains 70/100 with scheduler enrollment +unverified. Documentation drift coverage is the generated command/version contract only, +not complete semantic coverage of the repository. + +## Final local gate + +The serialized `pnpm check` at `2026-09-18T04-54-51-203Z-full.json` passed all eight gates +with `sourceChangedDuringValidation=false` and no unexecuted checks: + +- Node: **1035 passed, 0 failed, 0 cancelled, 0 skipped**. +- Rust: **554 passed, 0 failed, 343 ignored**; this is one Windows workspace invocation, + not a sum of overlapping hosted matrices. +- All 41 migration checks, documentation contract, formatting, Clippy, web build and lint passed. +- Gitleaks snapshot scan and the independent redacted synthetic-secret rejection control passed. +- Four YAML configurations parsed successfully; hosted execution remains unproven. + +The [portable final validation receipt](reports/ai-readiness-validation-iteration-2.json) +records the exact validated source fingerprint. Receipt/ledger additions after that run +are reporting-only changes, not retroactive changes to what the run verified. + +An intervening validation attempt exposed the helper rejecting pnpm's native `.mjs` entry +point after Node/Rust checks passed. The accepted entry points now include `.js`, `.cjs`, +`.mjs` and `.exe`, use explicit argument vectors without a shell, and tool-resolution +failures are recorded instead of escaping before the receipt is saved. Regression tests +and the complete final gate passed; the earlier attempt is not reclassified as successful. + +The branch remains a local candidate: no commit, push, PR, ruleset update, hosted dispatch, +live secret rotation, Teams activation or deployment has been performed. The next external +step needs its own exact preview/approval, including any protected-environment configuration. + +This paragraph describes the end of the local optimization pass. On September 18 the user +subsequently authorized draft PR publication and review of relevant open work. Publication +links and current CI belong in the PR; old benchmark rows remain historical observations, +not a new score for a later publication head. Repository-settings changes still receive an +exact preview and separate confirmation before application. + +The final benchmark retained 67.1 rather than reporting an assumed increase. Its operation +breakdown moved maintenance automation from 2 to 1 and continuous improvement from 1 to 2, +leaving the same total; keep this judge variance visible rather than choosing only favorable +dimensions. Further identical re-runs are not engineering progress. The remaining large +gains need the scoped operational proofs in the blocker table. Hourly synchronization is +restored after this pass, but its existing guard skips this worktree while changes remain +uncommitted; it never discards them or silently publishes the candidate. + +Publication review also enrolls `.github/skills/` native package tests so PR #304's integrity +gate is not lost when both contributions land. This does not import or approve that draft's +review policy. The new PR is a partial, non-closing contribution to existing issue #317; +its JUnit, broader version-contract and hosted Windows criteria remain separately tracked. + +The publication follow-up passed 15 focused helper/CLI tests and all six required repository +gates (41 migrations; format/Clippy; 554 Rust passed, 343 ignored; web build/lint). The fresh +broad Node invocation at `2026-09-18T05-24-29-620Z-full.json` passed 1035 cases and cancelled +one existing 15-second Teams SDK case; it is not recorded as a green full gate. This known +intermittent failure, clean-bootstrap limitation and independent review keep publication draft. +The earlier 67.1 result remains a historical candidate score, not a fresh published-head score. + +After the exact settings preview was approved, native GitHub security and main-branch +controls were enabled and verified. The scoped #323 checkpoint-test portability repair, +regressions, retained Teams timeout and separate required-gate results are recorded in the +[September 18 follow-up](reports/ai-readiness-portability-followup-2026-09-18.md). +This supersedes the earlier not-yet-applied settings status, not the historical benchmark. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md new file mode 100644 index 00000000..7b26321a --- /dev/null +++ b/docs/VALIDATION.md @@ -0,0 +1,60 @@ +# Reproducible repository validation + +Run from an isolated contribution worktree, not the configured source checkout. +Use `pnpm install --frozen-lockfile`; do not change the lockfile to hide an installation failure. +Install the steward's native SDK test dependency separately with +`npm ci --prefix scenarios/repo-steward --ignore-scripts --no-audit --no-fund`. +This installs test code only; it does not configure Teams credentials or send messages. +Node's native test runner is already the repository's JavaScript test harness. No alternate +framework or renamed tests are needed to make its existing regression suite discoverable. + +`pnpm check:preview` lists the exact checks without running them or writing artifacts. +`pnpm check:fast` checks migrations, documentation contracts and Rust formatting. +`pnpm test:js` runs the configured Node suites, including the web's TypeScript regressions. +`pnpm test` adds the Cargo workspace suite. `pnpm check` also runs Clippy, web build and lint. +The Node test runner uses one file worker to avoid competing ownership fixtures. +Install local hooks with `pre-commit install` only if you want them; CI does not rely on this. + + +Node: **24.19.0**. Rust: **1.98.1**. Package manager: **pnpm@11.19.0**. + +| Entry point | Implementation | +| --- | --- | +| `pnpm check:migrations` | `node tools/check_migrations.mjs` | +| `pnpm test` | `node tools/run_checks.mjs --group test` | +| `pnpm test:js` | `node tools/run_checks.mjs --group node` | +| `pnpm check:fast` | `node tools/run_checks.mjs --group fast` | +| `pnpm check:preview` | `node tools/run_checks.mjs --group full --dry-run` | +| `pnpm check:docs` | `node tools/check_docs.mjs` | +| `pnpm check:docs:preview` | `node tools/check_docs.mjs --dry-run` | +| `pnpm check:docs:write` | `node tools/check_docs.mjs --write` | +| `pnpm check` | `node tools/run_checks.mjs --group full` | + +Node test roots: `tools/`, `apps/web/src/`, `scenarios/repo-steward/`, `tests/readiness/`, `.github/skills/`. +Rust suite: `cargo test --workspace --locked`. + +Opt-in SQLx/native probes are not passes. Use only explicitly owned fixtures; never supply a retained application database. + + +## Evidence and limits + +The check driver emits a unique `output/readiness/*-{group}.json` receipt with command exit +codes, durations, source HEAD, dirty state, tracked-diff digest and untracked-file digests. +It stops after the first failing check and lists the remaining checks as not run. +Missing test output is unknown, never zero failures. Ignored/skipped tests are not passes. +Receipts exclude raw logs and environment values; inspect the terminal for a failing command. +The receipt is local evidence, not a signed attestation or proof of hosted CI or a browser journey. + +`test.config.json` controls native Node discovery and records the ordinary Cargo command. +Cargo compiles inline and macro-generated Rust tests; source annotation counts and filename +heuristics do not measure passing tests. Platform matrix totals overlap and must not be added. +Owned database/native probes remain separately opted in; never blanket-enable ignored tests. + +## Documentation drift + +The blocking local/CI gate compares the generated validation block with actual package scripts, +toolchain versions and test configuration. To repair a changed contract, preview with +`pnpm check:docs:preview`, review the generated block, run `pnpm check:docs:write`, then re-check. +Only the marked derived block can change. Missing or duplicate markers fail closed; authored +prose and historical evidence remain untouched. This deterministic contract coverage does not +claim repository-wide semantic drift coverage; architectural prose still requires review. diff --git a/docs/reports/ai-readiness-portability-followup-2026-09-18.md b/docs/reports/ai-readiness-portability-followup-2026-09-18.md new file mode 100644 index 00000000..4f7be721 --- /dev/null +++ b/docs/reports/ai-readiness-portability-followup-2026-09-18.md @@ -0,0 +1,94 @@ +# Readiness follow-up: GitHub controls and checkpoint-test portability + +This is a scoped follow-up to [draft PR #323](https://github.com/All-The-Vibes/ecorp/pull/323), +based on published head `6119296d475e94a45b643534a3a0bbd581b130f2`. It is a partial +contribution to #317, not a completed autonomous remediation system or a new benchmark. + +## Approved GitHub settings applied + +After the user approved the exact preview, native GitHub API readback confirmed the +following on September 18, 2026 (main-protection updated at `2026-09-18T05:42:15.733Z`): + +- Secret scanning and push protection enabled. +- Vulnerability alerts enabled (GET returned HTTP 204). +- Dependabot security updates enabled, not paused. +- Ruleset `23565201` (`main-protection`) requires the existing GitHub Actions + app's checks: `quality`, `integration`, `runner-platforms (ubuntu-latest)`, + `runner-platforms (windows-latest)`, `runner-platforms (macos-latest)` and + `desktop-windows`. Branches must be up to date. +- Code-owner review, stale-review dismissal, last-push approval and review-thread + resolution enabled. The existing one-approval count and required `ecorp-team` + review remain; no bypass was added. CODEOWNERS routing added by #323 becomes + main's routing policy only after its reviewed merge. + +The live ruleset was compared against every approved payload field. The other +three rulesets, default-branch restrictions, native CodeQL setup and thresholds +were preserved. CodeQL remains weekly with Actions, JavaScript/TypeScript, Python +and Rust. Auto-merge remains disabled. The new `secrets` check is not yet required +across main because its workflow has not landed there. + +These controls can block existing PRs with stale checks/reviews or unresolved +threads. They do not authorize merge, deployment, Factory intake or a new agent +controller. No existing human changes-requested review was dismissed. + +## Observed hosted failure and bounded repair + +On `6119296`, [quality job 105493434019](https://github.com/All-The-Vibes/ecorp/actions/runs/35311200797/job/105493434019) +reported 939 Node passes, 62 failures and five skips. The failures were in +`tools/e2e_checkpoint_verification.test.mjs`: a Windows-shaped in-memory runtime +fixture reached `expectedWorkspace` with Linux's host-default path implementation +and failed with `unsafe_posix_path`. The other original-head hosted checks +subsequently passed; this does not validate the follow-up head. + +The repair uses Node's existing `path.win32` and `path.posix` implementations: + +- Pure original/recovery assessment functions accept an explicit path API. +- The injectable offline suite forwards it through all assessment phases. +- Windows-shaped offline fixtures consistently use `path.win32`, independently + of the CI host. Dedicated regressions also exercise POSIX paths on every host. +- Native execution retains the host default. There is no new CLI override, + live-runtime configuration, path guessing or change to the containment helpers. + +Traversal, alternate data streams, UNC/incompatible spellings, sibling +workspaces and mismatched journal paths remain rejected. Independent review, +source identity, preserved-workspace and provider-free recovery checks remain +intact. No test is skipped and no safety check or timeout is relaxed. + +## Validation and remaining limitation + +- `node --test --test-reporter=tap tools/e2e_checkpoint_verification.test.mjs tools/e2e_stopped_source_checkpoint.test.mjs`: + **266 passed, zero failures, cancellations or skips**. +- Negative control: load the unchanged `6119296` driver in memory with the new + portability regressions. All four selected controls fail (three POSIX positive + paths plus the suite's incompatible-path rejection). They pass with the repair. + This uses inert in-memory fixtures, not a native service or provider. +- `pnpm check:preview`: all eight expected gates; no writes. +- Full run `2026-09-18T05-44-35-261Z-full.json`: **1053 Node passes, zero failures, + one cancellation**. The existing Teams SDK unauthenticated-request test timed + out at its unchanged 15-second limit. Source did not change during this run; + the fail-fast driver did not execute the subsequent Rust/web gates. +- The unchanged Teams suite immediately passed in isolation: **18/18**. That + does not erase the broad-suite timeout or establish consistently green CI. +- The six required repository gates were then run separately and all exited + successfully: 41 migration checks, Rust formatting, Clippy with warnings as + errors, workspace Rust tests, web build and web lint. Ignored database-dependent + Rust tests remain ignored, not passing coverage. + +The subsequent serialized full run `2026-09-18T05-48-29-340Z-full.json` passed all +eight gates with no source change during validation and no unexecuted gate: +**1054 Node passed, zero failed/cancelled/skipped; 554 Rust passed, zero failed, +343 ignored**. Migration, documentation contract, formatting, Clippy, web build +and web lint passed. The [unaltered portable receipt](ai-readiness-validation-portability.json) +retains the validated dirty-tree fingerprint. This result does not erase the +earlier Teams timeout. This reporting paragraph and the copied receipt were +added afterward; the two validated code/test files were unchanged. + +These checks validate contributor tooling, not browser/server/runner product +acceptance. No retained service, database, runner credential or original source +checkout was changed. Subsequent exact-head hosted results belong in #323; +the PR stays draft until remaining validation and independent review are complete. + +The last pre-publication CodeBlend result remains historical: composite 67.1, +foundations 87.0, operations 51.75. Neither settings activation nor this repair +is a measured score increase. Verified recurring repair/review/outcome evidence +remains separate work under #269/#280. diff --git a/docs/reports/ai-readiness-validation-iteration-1.json b/docs/reports/ai-readiness-validation-iteration-1.json new file mode 100644 index 00000000..97a6602b --- /dev/null +++ b/docs/reports/ai-readiness-validation-iteration-1.json @@ -0,0 +1,274 @@ +{ + "schemaVersion": 1, + "kind": "local-validation-report", + "scope": "Local readiness iteration 1, before subsequent CLI hardening. Not hosted CI, browser or production evidence.", + "producer": "Codex desktop task 01a0a0da-9366-7503-8c71-51034a99d053", + "startedAt": "2026-09-18T04:41:42.248Z", + "finishedAt": "2026-09-18T04:44:18.368Z", + "source": { + "commit": "0b1ad59da398e3dbd6a696d0264bcb6ebd620219", + "branch": "codex/ai-readiness-improvements", + "dirty": true, + "trackedDiffSha256": "ac16db3b92909eb959f296be8d39c8493e345b7918b2663c238c725919f88994", + "untrackedDigests": [ + [ + ".github/CODEOWNERS", + "998da9337ab183df57c3f61c890f31fea86371ac07729438e52bf795b684425c" + ], + [ + ".github/dependabot.yml", + "75e5ec094bc6c2f2ffe5c2ec1a1a39ff1cbd4208a2cc3de55af47650c66b9e0a" + ], + [ + ".github/skills/readiness-improvement/SKILL.md", + "deb98846d1e517a6a06cae3727c03bf654fe26fd5e794c592f7c8c8743553d74" + ], + [ + ".github/workflows/security.yml", + "cd6aa86c40946cb38a9361a6b455566c2ae5687394996aa2034778ffb7aa4278" + ], + [ + ".gitleaks.toml", + "3badf9b810f9cd9b40a971a7b8895ed426bb9743d57f2447a9b67679f8bbccfa" + ], + [ + ".node-version", + "7e8a2fa94951112b894a3dbe3d05efef5e9263741fa49125f0a70f40fedab4cc" + ], + [ + ".pre-commit-config.yaml", + "3d39bdaf0a12141d7fcd6cc44c551caec378e5c9cf2a2c6c8e9201fd8f60da31" + ], + [ + "apps/web/AGENTS.md", + "ea3eb78555a1aa841f134cf319f992441f269411ac2cb01e6bd5ce93b18daa18" + ], + [ + "crates/AGENTS.md", + "e0e99ca55a4bffbecf1f7e098c52c4d28293d97e5f4e35b83c45d278339d3e4f" + ], + [ + "docs/AI_READINESS.md", + "e7fd8c227f447155aa1e9d0f21ad86e313aa8e6add2bf5ed51f2df00aee9fa78" + ], + [ + "docs/VALIDATION.md", + "ce14e70cc8346c82d607ba398b3568b9ca14f762359e07477ee003f8bac30491" + ], + [ + "rust-toolchain.toml", + "887f9be066a15585a2c583578e84b0fcb541126d81546276bad3d2ff00d61167" + ], + [ + "test.config.json", + "00f3c4a9cac0634731296749e622e82dc0b520dd3285b28be520a063da87fed0" + ], + [ + "tools/check_docs.mjs", + "fe8edfa5fd093d2687a7c4bf03ff8b192c8e671f6dbf3a6b4492d8b180366fef" + ], + [ + "tools/readiness_contract.test.mjs", + "861311d6436f80f05244d37cd47773ecef7575f85c3c5c9880a8718a5e51d2e1" + ], + [ + "tools/run_checks.mjs", + "a8179688b23c21a2584d869157cde9b5fadd389b2ea6bda050a6c1653ddf943c" + ], + [ + "tools/security_canary.mjs", + "abb3a7047225ecf9136cd7a4af35f32afa509bfd3a6d2b8dcd183c8a11bfbcf1" + ] + ] + }, + "sourceChangedDuringValidation": false, + "status": "passed", + "checks": [ + { + "name": "migrations", + "argv": [ + "node", + "tools/check_migrations.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 135, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "docs", + "argv": [ + "node", + "tools/check_docs.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 111, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "node-tests", + "argv": [ + "node", + "--test", + "--test-concurrency=1", + "--test-reporter=tap", + "apps/web/src/evidenceNavigation.test.mjs", + "apps/web/src/evidenceSelection.test.mjs", + "apps/web/src/factoryCheckpointRecovery.test.mjs", + "apps/web/src/factoryControllerSelection.test.mjs", + "apps/web/src/factoryPolling.test.mjs", + "apps/web/src/missionCollaboration.test.mjs", + "apps/web/src/missionComposer.test.mjs", + "apps/web/src/missionOriginContext.test.mjs", + "apps/web/src/missionPreview.test.mjs", + "apps/web/src/missionProjection.test.mjs", + "apps/web/src/missionResultContext.test.mjs", + "apps/web/src/missionRuntime.test.mjs", + "apps/web/src/office/officeModel.test.mjs", + "apps/web/src/runActivity.test.mjs", + "apps/web/src/snapshotRefresh.test.mjs", + "apps/web/src/workResultCard.test.mjs", + "apps/web/src/workflowContext.test.mjs", + "apps/web/src/workspaceConnections.test.mjs", + "scenarios/repo-steward/actions.test.mjs", + "scenarios/repo-steward/steward.test.mjs", + "scenarios/repo-steward/teams-host.test.mjs", + "tools/checkpoint_application_fixture.test.mjs", + "tools/copilot_fs_wire_observer.test.mjs", + "tools/copilot_fs_wire_process.test.mjs", + "tools/copilot_probe_boundaries.test.mjs", + "tools/copilot_probe_environment.test.mjs", + "tools/copilot_probe_native_read.test.mjs", + "tools/copilot_probe_process.test.mjs", + "tools/e2e_checkpoint_verification.test.mjs", + "tools/e2e_evidence_selection.test.mjs", + "tools/e2e_external_adapters.test.mjs", + "tools/e2e_late_termination.test.mjs", + "tools/e2e_stopped_source_checkpoint.test.mjs", + "tools/fake_codex_budget_stream.test.mjs", + "tools/fake_github_quota.test.mjs", + "tools/fixture_source_identity.test.mjs", + "tools/local_stack_lifecycle.test.mjs", + "tools/local_stack_operation.test.mjs", + "tools/office_model.test.mjs", + "tools/owned_test_stack.test.mjs", + "tools/readiness_contract.test.mjs", + "tools/runner_ack_fault_relay.test.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 51734, + "counts": { + "rust": null, + "node": { + "tests": 1027, + "passed": 1027, + "failed": 0, + "skipped": 0, + "todo": 0, + "cancelled": 0 + } + }, + "errorCode": null + }, + { + "name": "format", + "argv": [ + "cargo", + "fmt", + "--check" + ], + "exitCode": 0, + "passed": true, + "durationMs": 5826, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "clippy", + "argv": [ + "cargo", + "clippy", + "--workspace", + "--all-targets", + "--locked", + "--", + "-D", + "warnings" + ], + "exitCode": 0, + "passed": true, + "durationMs": 17684, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "rust-tests", + "argv": [ + "cargo", + "test", + "--workspace", + "--locked" + ], + "exitCode": 0, + "passed": true, + "durationMs": 66216, + "counts": { + "rust": { + "passed": 554, + "failed": 0, + "ignored": 343, + "summaries": 14 + }, + "node": null + }, + "errorCode": null + }, + { + "name": "web-build", + "argv": [ + "pnpm", + "build:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 12283, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "web-lint", + "argv": [ + "pnpm", + "lint:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 2010, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + } + ], + "notRun": [] +} diff --git a/docs/reports/ai-readiness-validation-iteration-2.json b/docs/reports/ai-readiness-validation-iteration-2.json new file mode 100644 index 00000000..5f3384b0 --- /dev/null +++ b/docs/reports/ai-readiness-validation-iteration-2.json @@ -0,0 +1,283 @@ +{ + "schemaVersion": 1, + "kind": "local-validation-report", + "scope": "Local readiness iteration 2 after CLI hardening; later report/ledger additions are documentation-only. Not hosted CI, browser or production evidence.", + "producer": "Codex desktop task 01a0a0da-9366-7503-8c71-51034a99d053", + "startedAt": "2026-09-18T04:54:51.203Z", + "finishedAt": "2026-09-18T04:56:55.935Z", + "source": { + "commit": "0b1ad59da398e3dbd6a696d0264bcb6ebd620219", + "branch": "codex/ai-readiness-improvements", + "dirty": true, + "trackedDiffSha256": "ac16db3b92909eb959f296be8d39c8493e345b7918b2663c238c725919f88994", + "untrackedDigests": [ + [ + ".github/CODEOWNERS", + "998da9337ab183df57c3f61c890f31fea86371ac07729438e52bf795b684425c" + ], + [ + ".github/dependabot.yml", + "75e5ec094bc6c2f2ffe5c2ec1a1a39ff1cbd4208a2cc3de55af47650c66b9e0a" + ], + [ + ".github/skills/readiness-improvement/SKILL.md", + "deb98846d1e517a6a06cae3727c03bf654fe26fd5e794c592f7c8c8743553d74" + ], + [ + ".github/workflows/security.yml", + "cd6aa86c40946cb38a9361a6b455566c2ae5687394996aa2034778ffb7aa4278" + ], + [ + ".gitleaks.toml", + "3badf9b810f9cd9b40a971a7b8895ed426bb9743d57f2447a9b67679f8bbccfa" + ], + [ + ".node-version", + "7e8a2fa94951112b894a3dbe3d05efef5e9263741fa49125f0a70f40fedab4cc" + ], + [ + ".pre-commit-config.yaml", + "3d39bdaf0a12141d7fcd6cc44c551caec378e5c9cf2a2c6c8e9201fd8f60da31" + ], + [ + "apps/web/AGENTS.md", + "ea3eb78555a1aa841f134cf319f992441f269411ac2cb01e6bd5ce93b18daa18" + ], + [ + "crates/AGENTS.md", + "e0e99ca55a4bffbecf1f7e098c52c4d28293d97e5f4e35b83c45d278339d3e4f" + ], + [ + "docs/AI_READINESS.md", + "07698bde9623930a2146117d2574d801549cb4723614fa19933cd5aba079632b" + ], + [ + "docs/VALIDATION.md", + "c4ff483a1ff521a97daba5ef46e91d308e552e63d0758b63e1c413a2a789968d" + ], + [ + "docs/reports/ai-readiness-validation-iteration-1.json", + "e1255a08439e4ab0c7252cb6f7dfb039bd7dcc6a244ece4e549286e823d83aa7" + ], + [ + "rust-toolchain.toml", + "887f9be066a15585a2c583578e84b0fcb541126d81546276bad3d2ff00d61167" + ], + [ + "test.config.json", + "aa9e6194c9a1cb5620c8cdd3e447ac339863e865eabb737ee41c98d66daa4d95" + ], + [ + "tests/readiness/cli.test.js", + "737f4e5af63bd120ca938a8e59897967f54b2a6b6af62a2dcff773d1365c56ff" + ], + [ + "tools/check_docs.mjs", + "7c0660e9a13d9eeae285162eeb0cfa7b20d8ea9d413d3639f23ad9e3c8d17392" + ], + [ + "tools/readiness_contract.test.mjs", + "9ae16a5453ba4b0445673631a7d86580790f2d54ff808064283f61dbac975312" + ], + [ + "tools/run_checks.mjs", + "8785936fe89dd0fae2fbde15644bd4bb37b57ec1703544deaf124c0b3b359297" + ], + [ + "tools/security_canary.mjs", + "abb3a7047225ecf9136cd7a4af35f32afa509bfd3a6d2b8dcd183c8a11bfbcf1" + ] + ] + }, + "sourceChangedDuringValidation": false, + "status": "passed", + "checks": [ + { + "name": "migrations", + "argv": [ + "node", + "tools/check_migrations.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 153, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "docs", + "argv": [ + "node", + "tools/check_docs.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 110, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "node-tests", + "argv": [ + "node", + "--test", + "--test-concurrency=1", + "--test-reporter=tap", + "apps/web/src/evidenceNavigation.test.mjs", + "apps/web/src/evidenceSelection.test.mjs", + "apps/web/src/factoryCheckpointRecovery.test.mjs", + "apps/web/src/factoryControllerSelection.test.mjs", + "apps/web/src/factoryPolling.test.mjs", + "apps/web/src/missionCollaboration.test.mjs", + "apps/web/src/missionComposer.test.mjs", + "apps/web/src/missionOriginContext.test.mjs", + "apps/web/src/missionPreview.test.mjs", + "apps/web/src/missionProjection.test.mjs", + "apps/web/src/missionResultContext.test.mjs", + "apps/web/src/missionRuntime.test.mjs", + "apps/web/src/office/officeModel.test.mjs", + "apps/web/src/runActivity.test.mjs", + "apps/web/src/snapshotRefresh.test.mjs", + "apps/web/src/workResultCard.test.mjs", + "apps/web/src/workflowContext.test.mjs", + "apps/web/src/workspaceConnections.test.mjs", + "scenarios/repo-steward/actions.test.mjs", + "scenarios/repo-steward/steward.test.mjs", + "scenarios/repo-steward/teams-host.test.mjs", + "tests/readiness/cli.test.js", + "tools/checkpoint_application_fixture.test.mjs", + "tools/copilot_fs_wire_observer.test.mjs", + "tools/copilot_fs_wire_process.test.mjs", + "tools/copilot_probe_boundaries.test.mjs", + "tools/copilot_probe_environment.test.mjs", + "tools/copilot_probe_native_read.test.mjs", + "tools/copilot_probe_process.test.mjs", + "tools/e2e_checkpoint_verification.test.mjs", + "tools/e2e_evidence_selection.test.mjs", + "tools/e2e_external_adapters.test.mjs", + "tools/e2e_late_termination.test.mjs", + "tools/e2e_stopped_source_checkpoint.test.mjs", + "tools/fake_codex_budget_stream.test.mjs", + "tools/fake_github_quota.test.mjs", + "tools/fixture_source_identity.test.mjs", + "tools/local_stack_lifecycle.test.mjs", + "tools/local_stack_operation.test.mjs", + "tools/office_model.test.mjs", + "tools/owned_test_stack.test.mjs", + "tools/readiness_contract.test.mjs", + "tools/runner_ack_fault_relay.test.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 54838, + "counts": { + "rust": null, + "node": { + "tests": 1035, + "passed": 1035, + "failed": 0, + "skipped": 0, + "todo": 0, + "cancelled": 0 + } + }, + "errorCode": null + }, + { + "name": "format", + "argv": [ + "cargo", + "fmt", + "--check" + ], + "exitCode": 0, + "passed": true, + "durationMs": 1311, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "clippy", + "argv": [ + "cargo", + "clippy", + "--workspace", + "--all-targets", + "--locked", + "--", + "-D", + "warnings" + ], + "exitCode": 0, + "passed": true, + "durationMs": 923, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "rust-tests", + "argv": [ + "cargo", + "test", + "--workspace", + "--locked" + ], + "exitCode": 0, + "passed": true, + "durationMs": 57414, + "counts": { + "rust": { + "passed": 554, + "failed": 0, + "ignored": 343, + "summaries": 14 + }, + "node": null + }, + "errorCode": null + }, + { + "name": "web-build", + "argv": [ + "pnpm", + "build:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 8045, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "web-lint", + "argv": [ + "pnpm", + "lint:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 1788, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + } + ], + "notRun": [] +} diff --git a/docs/reports/ai-readiness-validation-portability.json b/docs/reports/ai-readiness-validation-portability.json new file mode 100644 index 00000000..e0411bc6 --- /dev/null +++ b/docs/reports/ai-readiness-validation-portability.json @@ -0,0 +1,211 @@ +{ + "schemaVersion": 1, + "group": "full", + "startedAt": "2026-09-18T05:48:29.340Z", + "source": { + "commit": "6119296d475e94a45b643534a3a0bbd581b130f2", + "branch": "codex/ai-readiness-improvements", + "dirty": true, + "trackedDiffSha256": "01e2bfba851b6d2e7715eb322265c10ddee4df324dee1cc26365daa562e1cc0e", + "untrackedDigests": [ + [ + "docs/reports/ai-readiness-portability-followup-2026-09-18.md", + "3bd3d63ee6ddbd3cbb7b4453ec4ad9394ce1636ece2cd75df890990403ee5a4c" + ] + ] + }, + "node": "24.19.0", + "checks": [ + { + "name": "migrations", + "argv": [ + "node", + "tools/check_migrations.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 117, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "docs", + "argv": [ + "node", + "tools/check_docs.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 103, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "node-tests", + "argv": [ + "node", + "--test", + "--test-concurrency=1", + "--test-reporter=tap", + "apps/web/src/evidenceNavigation.test.mjs", + "apps/web/src/evidenceSelection.test.mjs", + "apps/web/src/factoryCheckpointRecovery.test.mjs", + "apps/web/src/factoryControllerSelection.test.mjs", + "apps/web/src/factoryPolling.test.mjs", + "apps/web/src/missionCollaboration.test.mjs", + "apps/web/src/missionComposer.test.mjs", + "apps/web/src/missionOriginContext.test.mjs", + "apps/web/src/missionPreview.test.mjs", + "apps/web/src/missionProjection.test.mjs", + "apps/web/src/missionResultContext.test.mjs", + "apps/web/src/missionRuntime.test.mjs", + "apps/web/src/office/officeModel.test.mjs", + "apps/web/src/runActivity.test.mjs", + "apps/web/src/snapshotRefresh.test.mjs", + "apps/web/src/workResultCard.test.mjs", + "apps/web/src/workflowContext.test.mjs", + "apps/web/src/workspaceConnections.test.mjs", + "scenarios/repo-steward/actions.test.mjs", + "scenarios/repo-steward/steward.test.mjs", + "scenarios/repo-steward/teams-host.test.mjs", + "tests/readiness/cli.test.js", + "tools/checkpoint_application_fixture.test.mjs", + "tools/copilot_fs_wire_observer.test.mjs", + "tools/copilot_fs_wire_process.test.mjs", + "tools/copilot_probe_boundaries.test.mjs", + "tools/copilot_probe_environment.test.mjs", + "tools/copilot_probe_native_read.test.mjs", + "tools/copilot_probe_process.test.mjs", + "tools/e2e_checkpoint_verification.test.mjs", + "tools/e2e_evidence_selection.test.mjs", + "tools/e2e_external_adapters.test.mjs", + "tools/e2e_late_termination.test.mjs", + "tools/e2e_stopped_source_checkpoint.test.mjs", + "tools/fake_codex_budget_stream.test.mjs", + "tools/fake_github_quota.test.mjs", + "tools/fixture_source_identity.test.mjs", + "tools/local_stack_lifecycle.test.mjs", + "tools/local_stack_operation.test.mjs", + "tools/office_model.test.mjs", + "tools/owned_test_stack.test.mjs", + "tools/readiness_contract.test.mjs", + "tools/runner_ack_fault_relay.test.mjs" + ], + "exitCode": 0, + "passed": true, + "durationMs": 51383, + "counts": { + "rust": null, + "node": { + "tests": 1054, + "passed": 1054, + "failed": 0, + "skipped": 0, + "todo": 0, + "cancelled": 0 + } + }, + "errorCode": null + }, + { + "name": "format", + "argv": [ + "cargo", + "fmt", + "--check" + ], + "exitCode": 0, + "passed": true, + "durationMs": 1237, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "clippy", + "argv": [ + "cargo", + "clippy", + "--workspace", + "--all-targets", + "--locked", + "--", + "-D", + "warnings" + ], + "exitCode": 0, + "passed": true, + "durationMs": 1061, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "rust-tests", + "argv": [ + "cargo", + "test", + "--workspace", + "--locked" + ], + "exitCode": 0, + "passed": true, + "durationMs": 57168, + "counts": { + "rust": { + "passed": 554, + "failed": 0, + "ignored": 343, + "summaries": 14 + }, + "node": null + }, + "errorCode": null + }, + { + "name": "web-build", + "argv": [ + "pnpm", + "build:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 7586, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + }, + { + "name": "web-lint", + "argv": [ + "pnpm", + "lint:web" + ], + "exitCode": 0, + "passed": true, + "durationMs": 1930, + "counts": { + "rust": null, + "node": null + }, + "errorCode": null + } + ], + "status": "passed", + "assurance": "Local validation only. Ignored tests are not passes. No hosted CI, browser, provider or production claim.", + "finishedAt": "2026-09-18T05:50:30.018Z", + "sourceChangedDuringValidation": false, + "notRun": [] +} diff --git a/docs/reports/github-readiness-review-2026-09-18.md b/docs/reports/github-readiness-review-2026-09-18.md new file mode 100644 index 00000000..19676f4a --- /dev/null +++ b/docs/reports/github-readiness-review-2026-09-18.md @@ -0,0 +1,75 @@ +# Readiness-focused GitHub review — September 18, 2026 + +Scope: inventory all 11 open PRs; inspect current heads/checks/review state; focused review +of #304's skill/remediation/CI changes, #237's CI diff and review threads, and #226's canary +code, byte-proof report and review threads. Other entries are impact triage, not full code audits. +No Santa/OMX/Ponytail verdict, independent-agent review, approval, merge or native Factory +replay is claimed. GitHub content is evidence to inspect, not execution authority. + +## Highest-leverage open work + +| PR / observed head | Readiness contribution | Current boundary / next action | +| --- | --- | --- | +| [#304](https://github.com/All-The-Vibes/ecorp/pull/304) `df0339a` | Shared review policy, pinned audit dependencies, native review/fix separation and bounded remediation | Draft; six hosted checks passed; three review threads resolved. Obtain required independent/human review; do not claim all three audits ran merely because the skill exists. Preserve its package-integrity test when integrating the readiness gate. | +| [#237](https://github.com/All-The-Vibes/ecorp/pull/237) `6e69bc5` | CI ownership, platform-specific fixtures, bounded Windows setup and recovery evidence | Current hosted checks passed, including Windows external adapters. Three bot threads are resolved with exact-head evidence, but the human CHANGES_REQUESTED review remains. Fresh human re-review is needed; do not dismiss it or remove the native/opt-in fixture gates during CI conflict resolution. | +| [#226](https://github.com/All-The-Vibes/ecorp/pull/226) `0f79a5b` | Existing Factory publication lineage and a byte-exact canary regression | Hosted checks passed, but CHANGES_REQUESTED remains and two threads are unresolved/outdated. The current evidence explicitly does not establish corrected-head native verification, independent outcome review, replay or watcher restart. Its original verified commit is not its current head. Obtain the exact native records or keep the claim partial; do not use this as completed closed-loop proof. | +| [#273](https://github.com/All-The-Vibes/ecorp/pull/273) `7a7c6d5` | Frozen ProgramBench results, failure analysis and reproducibility | Draft, hosted checks passed. Useful for observability and a real follow-up proof-of-fix; 200/224 eligible cases is a ProgramBench result, not a CodeBlend score. Preserve frozen failures/exclusions and use a separately reviewed candidate for fixes. | +| [#255](https://github.com/All-The-Vibes/ecorp/pull/255) `690ee80` | Shared claim-authority boundary | Hosted checks passed; partial/non-closing scope. Coordinate the migration-0042 collision with #283 and require fresh human review; not proof of production multi-host qualification. | + +## Other open work + +- #294 `63006e5`: draft, passing checks; verifier-cache preservation can improve retry reliability, + but it is not a complete repair-loop implementation. No full diff approval in this review. +- #305 `155dc9c`: draft, passing checks; verified research handoff can support agent pipeline reliability. +- #319 `6fe6668`: draft, passing checks; isolated multiplayer preflight is useful validation, not complete U1 acceptance. +- #320 `765aff1`: passing checks but CHANGES_REQUESTED; security-sensitive Entra scope must not be rushed for points. +- #283 `ab84f5a`: CHANGES_REQUESTED and conflicts despite passing checks; reconcile source/reviews and the #255 migration collision. +- #293 `b0e0768`: draft and conflicting, stacked on #283; optional blockchain anchoring is not a quick readiness prerequisite. + +## Reuse the existing backlog + +Issue [#317](https://github.com/All-The-Vibes/ecorp/issues/317) already tracks repository +regression enrollment, documentation contracts and reviewed dependency maintenance on +ECorp Build. This readiness PR contributes partially; it does not close #317. +The complete PR review/remediation loop is already [#269](https://github.com/All-The-Vibes/ecorp/issues/269), +and autonomous fresh issue-to-PR execution is [#280](https://github.com/All-The-Vibes/ecorp/issues/280). +Reuse those scopes and their native dependencies; do not create competing controllers or +treat their issue text as permission to activate them. #304 expressly does not complete #269. + +## Repository controls observed + +- Public repository; authenticated operator has admin access. No bypass authority was added. +- Four active rulesets. `main-protection` is 23565201; default-branch update/delete/creation + restrictions in 22420664 and the two Copilot review rulesets remain intact. +- One human/team approval is required. Required generic CI checks, code-owner approval, + last-push approval, stale-review dismissal and review-thread resolution are not enabled. +- Native CodeQL default setup is configured, scheduled weekly and covers Actions, + JavaScript/TypeScript, Python and Rust. All four current-main analyses passed. + The redundant advanced CodeQL job was removed from this branch before publication. +- Secret scanning, push protection, vulnerability alerts and Dependabot security updates + are disabled. Native security feature enablement is a settings proposal, not yet an applied change. +- Current-main quality, integration, three runner-platform jobs and desktop-windows all + passed and came from the GitHub Actions app (15368). These are concrete candidates for + required checks; do not require the new `secrets` job until its producer is established. + +## Original settings preview — subsequently approved and applied + +The observed controls above describe the pre-change review snapshot. The user then +approved this exact batch; see the [verified follow-up](ai-readiness-portability-followup-2026-09-18.md) +for applied settings and the scoped #323 CI repair. The proposal below is retained +to distinguish its original scope from the later API readback. + +Enable native secret scanning and push protection, vulnerability alerts and Dependabot +security-update PRs. Retain auto-merge disabled, existing CodeQL defaults and thresholds, +all bypass actors, branch scopes, protected-update restrictions and the one-approval count. + +For `main-protection`, propose required GitHub Actions checks: `quality`, `integration`, +`runner-platforms (ubuntu-latest)`, `runner-platforms (windows-latest)`, +`runner-platforms (macos-latest)` and `desktop-windows`; require up-to-date branches, +code-owner approval, a reviewer other than the last pusher, dismissal of stale approvals, +and resolution of review threads. The same eligible review may satisfy the overlapping +team/owner requirements; this proposal does not increase the approval count. + +This affects all PRs targeting main: stale branches/reviews and unresolved discussions +may block merge. Refresh and compare the exact live ruleset before applying an approved +payload. No setting grants agents write authority, launches Factory, merges or deploys. diff --git a/package.json b/package.json index f1c7eaad..1547c029 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,19 @@ "packageManager": "pnpm@11.19.0", "scripts": { "check:migrations": "node tools/check_migrations.mjs", + "test": "node tools/run_checks.mjs --group test", + "test:js": "node tools/run_checks.mjs --group node", + "check:fast": "node tools/run_checks.mjs --group fast", + "check:preview": "node tools/run_checks.mjs --group full --dry-run", + "check:docs": "node tools/check_docs.mjs", + "check:docs:preview": "node tools/check_docs.mjs --dry-run", + "check:docs:write": "node tools/check_docs.mjs --write", "dev:web": "pnpm --dir apps/web dev", "build:web": "pnpm --dir apps/web build", "lint:web": "pnpm --dir apps/web lint", - "check": "pnpm check:migrations && cargo fmt --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && pnpm build:web && pnpm lint:web" + "check": "node tools/run_checks.mjs --group full" + }, + "devDependencies": { + "ws": "8.18.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5393186a..9a042d46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + devDependencies: + ws: + specifier: 8.18.3 + version: 8.18.3 apps/web: dependencies: @@ -492,6 +496,9 @@ packages: yaml: optional: true + ws@8.18.3: + resolution: {integrity: sha1-tWuIq//eYnkcY5FwQAyT3LDJVHI=} + snapshots: '@oxc-project/types@0.146.0': {} @@ -762,3 +769,5 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 fsevents: 2.3.3 + + ws@8.18.3: {} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..c3f67b67 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.98.1" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/test.config.json b/test.config.json new file mode 100644 index 00000000..69d36b45 --- /dev/null +++ b/test.config.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "nodeTestRoots": ["tools/", "apps/web/src/", "scenarios/repo-steward/", "tests/readiness/", ".github/skills/"], + "nodeTestSuffixes": [".test.mjs", ".test.js"], + "rustCommand": ["cargo", "test", "--workspace", "--locked"], + "ignoredTests": "Opt-in SQLx/native probes are not passes. Use only explicitly owned fixtures; never supply a retained application database." +} diff --git a/tests/readiness/cli.test.js b/tests/readiness/cli.test.js new file mode 100644 index 00000000..b8e98f64 --- /dev/null +++ b/tests/readiness/cli.test.js @@ -0,0 +1,90 @@ +// Black-box tests of the actual validation CLI; fixture counts are not product-test counts. +const assert = require('node:assert/strict') +const test = require('node:test') +const { spawnSync } = require('node:child_process') +const { mkdtempSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, readdirSync, rmSync } = require('node:fs') +const { tmpdir } = require('node:os') +const path = require('node:path') +const source = path.resolve(__dirname, '../..') + +function fixture(t, testBody = "import test from 'node:test'; test('owned fixture', () => {})") { + const root = mkdtempSync(path.join(tmpdir(), 'ecorp-readiness-cli-')) + t.after(() => { + const relative = path.relative(tmpdir(), root) + assert.ok(relative.startsWith('ecorp-readiness-cli-') && !relative.includes(path.sep)) + rmSync(root, { recursive: true }) + }) + for (const directory of ['tools', 'docs']) mkdirSync(path.join(root, directory)) + for (const file of ['tools/run_checks.mjs', 'tools/check_docs.mjs', 'test.config.json', 'package.json', '.node-version', 'rust-toolchain.toml', 'docs/VALIDATION.md']) { + copyFileSync(path.join(source, file), path.join(root, file)) + } + writeFileSync(path.join(root, '.gitignore'), 'output/\n') + writeFileSync(path.join(root, 'tools/fixture.test.mjs'), testBody) + writeFileSync(path.join(root, 'tools/check_migrations.mjs'), "console.error('synthetic gate rejection'); process.exit(1)") + const git = args => { + const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' }) + assert.equal(result.status, 0, result.stderr) + return result.stdout + } + git(['init', '--quiet']) + git(['add', '.']) + git(['-c', 'user.name=Readiness CLI fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'isolated test fixture']) + const childEnvironment = { ...process.env } + // Each black-box invocation is a new CLI, not a nested Node test worker. + delete childEnvironment.NODE_TEST_CONTEXT + return { root, git, invoke: args => spawnSync(process.execPath, ['tools/run_checks.mjs', ...args], { cwd: root, env: childEnvironment, encoding: 'utf8', timeout: 20000 }) } +} + +function receipt(root) { + const directory = path.join(root, 'output/readiness') + const files = readdirSync(directory).filter(file => file.endsWith('.json')) + assert.equal(files.length, 1) + return JSON.parse(readFileSync(path.join(directory, files[0]), 'utf8')) +} + +test('dry run executes no checks, creates no receipt and leaves source unchanged', t => { + const f = fixture(t), before = f.git(['status', '--porcelain']) + const result = f.invoke(['--group', 'full', '--dry-run']) + assert.equal(result.status, 0, result.stderr) + const preview = JSON.parse(result.stdout) + assert.equal(preview.dry_run, true) + assert.deepEqual(preview.writes, []) + assert.equal(preview.checks.length, 8) + assert.equal(f.git(['status', '--porcelain']), before) + assert.ok(!readdirSync(f.root).includes('output')) +}) +test('unknown arguments cannot become commands', t => { + const f = fixture(t) + const result = f.invoke(['--group', 'node', '--anything']) + assert.equal(result.status, 1) + assert.match(result.stderr, /Usage:/) + assert.ok(!readdirSync(f.root).includes('output')) +}) +test('successful native tests produce observed counts and stable-source evidence', t => { + const f = fixture(t), result = f.invoke(['--group', 'node']) + assert.equal(result.status, 0, result.stderr) + const report = receipt(f.root) + assert.equal(report.status, 'passed') + assert.equal(report.sourceChangedDuringValidation, false) + assert.equal(report.checks[0].counts.node.passed, 1) + assert.equal(report.checks[0].counts.node.cancelled, 0) + assert.deepEqual(report.notRun, []) +}) +test('failing first gate stops execution and marks later gates not run', t => { + const f = fixture(t), result = f.invoke(['--group', 'fast']) + assert.equal(result.status, 1) + const report = receipt(f.root) + assert.equal(report.status, 'failed') + assert.equal(report.checks.length, 1) + assert.equal(report.checks[0].name, 'migrations') + assert.deepEqual(report.notRun, ['docs', 'format']) +}) +test('a passing test cannot certify source it modified during validation', t => { + const f = fixture(t, "import test from 'node:test'; import { writeFileSync } from 'node:fs'; test('mutation fixture', () => writeFileSync('new-source.txt', 'changed'))") + const result = f.invoke(['--group', 'node']) + assert.equal(result.status, 1) + const report = receipt(f.root) + assert.equal(report.checks[0].counts.node.passed, 1) + assert.equal(report.sourceChangedDuringValidation, true) + assert.equal(report.status, 'source_changed') +}) diff --git a/tools/check_docs.mjs b/tools/check_docs.mjs new file mode 100644 index 00000000..2eb91336 --- /dev/null +++ b/tools/check_docs.mjs @@ -0,0 +1,53 @@ +import { readFileSync, lstatSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +export const START = '' +export const END = '' + +export function renderContract(pkg, nodeVersion, rustVersion, config) { + if (!pkg.scripts?.test || !pkg.scripts?.['test:js'] || !pkg.scripts?.check) throw new Error('Required validation scripts missing') + if (!/^\d+\.\d+\.\d+$/u.test(nodeVersion) || !/^\d+\.\d+\.\d+$/u.test(rustVersion)) throw new Error('Toolchains must be exact versions') + const rows = Object.entries(pkg.scripts).filter(([name]) => /^(test|check)/u.test(name)) + .map(([name, command]) => `| \`pnpm ${name}\` | \`${command.replaceAll('|', '\\|')}\` |`) + return `${START}\nNode: **${nodeVersion}**. Rust: **${rustVersion}**. Package manager: **${pkg.packageManager}**.\n\n` + + `| Entry point | Implementation |\n| --- | --- |\n${rows.join('\n')}\n\n` + + `Node test roots: ${config.nodeTestRoots.map(value => `\`${value}\``).join(', ')}.\n` + + `Rust suite: \`${config.rustCommand.join(' ')}\`.\n\n${config.ignoredTests}\n${END}` +} + +export function replaceContract(document, generated) { + if (document.split(START).length !== 2 || document.split(END).length !== 2 || document.indexOf(START) > document.indexOf(END)) { + throw new Error('Exactly one ordered generated block is required; refusing to overwrite prose') + } + const eol = document.includes('\r\n') ? '\r\n' : '\n' + const nativeGenerated = generated.replaceAll(/\r?\n/gu, eol) + return document.slice(0, document.indexOf(START)) + nativeGenerated + document.slice(document.indexOf(END) + END.length) +} + +export function main(args = process.argv.slice(2)) { + if (args.length > 1 || args.some(arg => !['--dry-run', '--write'].includes(arg))) throw new Error('Usage: node tools/check_docs.mjs [--dry-run|--write]') + const target = path.join(root, 'docs', 'VALIDATION.md') + if (lstatSync(path.join(root, 'docs')).isSymbolicLink() || lstatSync(target).isSymbolicLink()) throw new Error('Documentation target cannot be redirected') + const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) + const config = JSON.parse(readFileSync(path.join(root, 'test.config.json'), 'utf8')) + const nodeVersion = readFileSync(path.join(root, '.node-version'), 'utf8').trim() + const rustVersion = readFileSync(path.join(root, 'rust-toolchain.toml'), 'utf8').match(/channel\s*=\s*"([^"]+)"/u)?.[1] + const generated = renderContract(pkg, nodeVersion, rustVersion, config) + const before = readFileSync(target, 'utf8') + const expected = replaceContract(before, generated) + if (args.includes('--dry-run')) { console.log(JSON.stringify({ dry_run: true, changed: before !== expected, target: 'docs/VALIDATION.md', generated }, null, 2)); return 0 } + if (before === expected) return 0 + if (!args.includes('--write')) { console.error('Documentation contract drift: run pnpm check:docs:preview, review, then pnpm check:docs:write'); return 1 } + // Only this derived block changes. Never rewrite authored or historical evidence. + if (readFileSync(target, 'utf8') !== before) throw new Error('Documentation changed during preparation') + writeFileSync(target, expected) + if (readFileSync(target, 'utf8') !== expected) throw new Error('Documentation write could not be verified') + console.log('Updated only the generated validation contract; rerun pnpm check:docs') + return 0 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + try { process.exitCode = main() } catch (error) { console.error(error.message); process.exitCode = 1 } +} diff --git a/tools/e2e_checkpoint_verification.mjs b/tools/e2e_checkpoint_verification.mjs index 4ebc6594..bff4370e 100644 --- a/tools/e2e_checkpoint_verification.mjs +++ b/tools/e2e_checkpoint_verification.mjs @@ -400,7 +400,7 @@ function oneEvent(events, type) { return matches[0] } -export function assessOriginal(state, replay, config, plan, identity) { +export function assessOriginal(state, replay, config, plan, identity, pathApi = path) { replaySummary(replay.events, replay.through, config.corp_id) const rows = ownedRows(state, config, plan, identity) ensure(rows?.run && rows.runs.length === 1, 'one_original_provider_run_required') @@ -484,9 +484,10 @@ export function assessOriginal(state, replay, config, plan, identity) { equal(preserved.payload.source_checkpoint, checkpoint, 'physical_checkpoint_source_or_policy_mismatch') ensure(run.workspace_branch === branch && run.workspace_base_commit === PIN.source_commit && run.workspace_base_ref === 'HEAD' && - samePath(run.workspace_path, expectedWorkspace(config, task.id, run.id)), 'original_workspace_lineage_mismatch') + samePath(run.workspace_path, expectedWorkspace(config, task.id, run.id, pathApi), pathApi), + 'original_workspace_lineage_mismatch') for (const event of [started, preserved]) { - ensure(samePath(event.payload.workspace, run.workspace_path) && + ensure(samePath(event.payload.workspace, run.workspace_path, pathApi) && event.payload.workspace_branch === branch && event.payload.workspace_base_ref === 'HEAD' && event.payload.workspace_base_commit === PIN.source_commit, 'native_workspace_event_mismatch') } @@ -519,7 +520,7 @@ function assertOriginalUnchanged(rows, replay, state, original) { original.incidents_sha256, 'original_policy_attempts_or_incidents_changed') } -export function assessRecovery(state, replay, context, config, plan, original, identity) { +export function assessRecovery(state, replay, context, config, plan, original, identity, pathApi = path) { replaySummary(replay.events, replay.through, config.corp_id) const rows = ownedRows(state, config, plan, identity) assertOriginalUnchanged(rows, replay, state, original) @@ -552,7 +553,7 @@ export function assessRecovery(state, replay, context, config, plan, original, i run.source_base_commit === PIN.source_commit && run.workspace_base_commit === PIN.source_commit && run.workspace_base_ref === 'HEAD' && run.workspace_branch === rows.run.workspace_branch && - samePath(run.workspace_path, rows.run.workspace_path) && run.workspace_disposition === 'preserved' && + samePath(run.workspace_path, rows.run.workspace_path, pathApi) && run.workspace_disposition === 'preserved' && run.workspace_fingerprint === original.source_checkpoint.workspace_fingerprint, 'not_exact_provider_free_workspace_lineage') for (const field of ['budget_tokens_limit', 'budget_cost_microusd_limit', 'input_tokens', @@ -577,7 +578,7 @@ export function assessRecovery(state, replay, context, config, plan, original, i const started = oneEvent(events, 'run.started') const verifying = oneEvent(events, 'run.verification_started') ensure(started.payload.execution_mode === 'verification_only' && - samePath(started.payload.workspace, rows.run.workspace_path) && + samePath(started.payload.workspace, rows.run.workspace_path, pathApi) && verifying.payload.check_count === plan.policy.checks.length && requested.seq < started.seq && started.seq < verifying.seq, 'native_verify_run_not_observed') const ack = events.filter((event) => event.type === 'runner.command_acknowledged' && @@ -611,7 +612,7 @@ export function assessRecovery(state, replay, context, config, plan, original, i request.payload.expected_head_commit === delivered?.head_commit, 'checkpoint_reattest_authority_mismatch') const proof = preserved[0].payload - ensure(samePath(proof.workspace, run.workspace_path) && proof.workspace_branch === run.workspace_branch && + ensure(samePath(proof.workspace, run.workspace_path, pathApi) && proof.workspace_branch === run.workspace_branch && proof.workspace_base_ref === 'HEAD' && proof.workspace_base_commit === PIN.source_commit && proof.workspace_fingerprint === original.source_checkpoint.workspace_fingerprint && proof.head_commit === request.payload.expected_head_commit && proof.branch_deleted === false && @@ -989,7 +990,10 @@ function admissionView(context, report) { } /** All effects are injectable. Tests use ONLY memory, never a fake service. */ -export async function executeSuite(config, report, io, { continuation = false, serverUpgrade = null } = {}) { +// Offline callers can select fixture path semantics; native execution keeps the host default. +export async function executeSuite(config, report, io, { + continuation = false, serverUpgrade = null, pathApi = path, +} = {}) { ensure(!serverUpgrade || continuation, 'server_upgrade_requires_continue') validateSavedReport(report, config, serverUpgrade) const plan = buildPlan(config, report.driver_id) @@ -1096,7 +1100,7 @@ export async function executeSuite(config, report, io, { continuation = false, s await poll('original_checkpoint', ({ rows }) => rows?.run && ['failed', 'cancelled'].includes(rows.run.status) && rows.run.workspace_disposition === 'preserved') const current = await quiet('original_quiet') - report.original = assessOriginal(current.state, current.replay, config, plan, report.identity) + report.original = assessOriginal(current.state, current.replay, config, plan, report.identity, pathApi) report.original.base_file = await io.readBase({ task_id: report.identity.task_id, run_id: report.identity.original_run_id, }) @@ -1123,7 +1127,7 @@ export async function executeSuite(config, report, io, { continuation = false, s (TERMINAL.has(replacement.status) || replacement.status === 'waiting_for_approval')) let current = await quiet('native_verifier_quiet') let result = assessRecovery(current.state, current.replay, report.original.admission, - config, plan, report.original, report.identity) + config, plan, report.original, report.identity, pathApi) report.replacement = result await save() if (!config.missing_artifact && !result.verified) { @@ -1153,7 +1157,7 @@ export async function executeSuite(config, report, io, { continuation = false, s } const final = await observe('final') report.replacement = assessRecovery(final.state, final.replay, report.original.admission, - config, plan, report.original, report.identity) + config, plan, report.original, report.identity, pathApi) const base = await io.readBase({ task_id: report.identity.task_id, run_id: report.identity.original_run_id }) equal(base, report.original.base_file, 'original_physical_base_file_changed') report.status = config.missing_artifact ? 'expected_missing_artifact_rejection' : 'verified_and_downloaded' diff --git a/tools/e2e_checkpoint_verification.test.mjs b/tools/e2e_checkpoint_verification.test.mjs index 8cc76b6f..f1a06f74 100644 --- a/tools/e2e_checkpoint_verification.test.mjs +++ b/tools/e2e_checkpoint_verification.test.mjs @@ -6,7 +6,7 @@ import { createHash } from 'node:crypto' import path from 'node:path' import { runInNewContext } from 'node:vm' import test from 'node:test' -import * as driver from './e2e_checkpoint_verification.mjs' +import * as nativeDriver from './e2e_checkpoint_verification.mjs' const id = (n) => `00000000-0000-4000-8000-${n.toString(16).padStart(12, '0')}` const clone = (value) => structuredClone(value) @@ -16,6 +16,15 @@ const canonical = (value) => JSON.stringify(value, (_, item) => ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item) const hashJson = (value) => sha(canonical(value)) const WIN = path.win32 +// The retained runtime fixture is Windows-shaped even when this offline suite runs on Linux. +// Inject its semantics without changing the native driver's host-default behavior. +const driver = { + ...nativeDriver, + assessOriginal: (...args) => nativeDriver.assessOriginal(...args, WIN), + assessRecovery: (...args) => nativeDriver.assessRecovery(...args, WIN), + executeSuite: (config, report, io, options = {}) => + nativeDriver.executeSuite(config, report, io, { ...options, pathApi: WIN }), +} const SENTINEL = 'SYNTHETIC_PRIVATE_TOKEN_AND_PROVIDER_OUTPUT_DO_NOT_PERSIST' const FIXTURE_SHA = 'c'.repeat(64) // Explicit current attestation; no fixture-file read. const BASE_SHA = 'f34848ca92665c342abd5816c9e3eda0e82180671195362bcd0080544a3bc2ac' @@ -262,9 +271,9 @@ for (const [label, mutate] of [ }) } -function recoveryFixture(c = config(), { approved = false, exportFailure = false } = {}) { - const f = originalFixture(c) - const original = exported('assessOriginal')(f.state, f.replay, c, f.plan, f.identity) +function recoveryFixture(c = config(), { approved = false, exportFailure = false, pathApi = WIN } = {}) { + const f = originalFixture(c, id(901), pathApi) + const original = nativeDriver.assessOriginal(f.state, f.replay, c, f.plan, f.identity, pathApi) const recoveryId = id(109) const replacementId = id(110) const failed = exportFailure || c.missing_artifact @@ -880,7 +889,7 @@ test('invokeFactory never retries an uncertain execution or logs raw stdout/stde } }) -function originalFixture(c = config(), driverId = id(901)) { +function originalFixture(c = config(), driverId = id(901), pathApi = WIN) { const plan = exported('buildPlan')(c, driverId) const state = emptyState(c) const missionId = id(101) @@ -931,7 +940,7 @@ function originalFixture(c = config(), driverId = id(901)) { adapter: 'codex', current_run_id: null, status: 'idle', retired_at: null, } const branch = `crony/task-${taskId.replaceAll('-', '')}/run-${runId.replaceAll('-', '')}` - const workspace = path.join(c.runner_root, 'worktrees', taskId.replaceAll('-', ''), runId.replaceAll('-', '')) + const workspace = pathApi.join(c.runner_root, 'worktrees', taskId.replaceAll('-', ''), runId.replaceAll('-', '')) const run = { id: runId, corp_id: c.corp_id, task_id: taskId, agent_id: agentId, runner_id: c.runner_id, execution_mode: 'provider', status, breaker_stage: stage, workspace_disposition: 'preserved', @@ -987,6 +996,84 @@ function originalFixture(c = config(), driverId = id(901)) { replay: { events, through: events.at(-1).seq }, identity: driver.newReport(c, driverId).identity } } +// Pure assessment fixtures exercise both implementations on every host. They do not +// authorize a POSIX runtime for the separately pinned Windows acceptance driver. +for (const [label, pathApi, root, malformed] of [ + ['Windows', WIN, EXPECTED.runner_root, [ + ['traversal', 'C:\\owned\\..\\escape', 'unsafe_windows_component'], + ['alternate data stream', 'C:\\owned\\file:stream', 'unsafe_windows_path'], + ['UNC', '\\\\server\\share\\workspace', 'unsafe_windows_path'], + ['POSIX spelling', '/owned/workspace', 'unsafe_windows_path'], + ]], + ['POSIX', path.posix, '/owned/runner-workspaces', [ + ['traversal', '/owned/../escape', 'unsafe_posix_path'], + ['double root', '//owned/workspace', 'unsafe_posix_path'], + ['backslash', '/owned/workspace\\escape', 'unsafe_posix_path'], + ['Windows spelling', 'C:\\owned\\workspace', 'unsafe_posix_path'], + ]], +]) { + const platformConfig = () => ({ ...config(), runner_root: root }) + test(`${label} original assessment uses the explicit path implementation`, () => { + const f = originalFixture(platformConfig(), id(901), pathApi) + const result = nativeDriver.assessOriginal(f.state, f.replay, f.c, f.plan, f.identity, pathApi) + assert.equal(result.run_sha256, hashJson(f.run)) + assert.equal(f.run.workspace_path, pathApi.join(root, 'worktrees', + f.task.id.replaceAll('-', ''), f.run.id.replaceAll('-', ''))) + }) + for (const approved of [false, true]) { + test(`${label} recovery preserves ${approved ? 'accepted' : 'pending'} independent review`, () => { + const f = recoveryFixture(platformConfig(), { approved, pathApi }) + const result = nativeDriver.assessRecovery(f.state, f.replay, f.context, + f.c, f.plan, f.original, f.identity, pathApi) + assert.equal(result.verified, approved) + assert.equal(result.new_provider_sessions, 0) + }) + } + for (const [kind, workspace, code] of malformed) { + test(`${label} assessment rejects ${kind} without normalizing it into authority`, () => { + const f = originalFixture(platformConfig(), id(901), pathApi) + f.run.workspace_path = workspace + assert.throws(() => nativeDriver.assessOriginal(f.state, f.replay, + f.c, f.plan, f.identity, pathApi), { code }) + const r = recoveryFixture(platformConfig(), { pathApi }) + r.replacement.workspace_path = workspace + assert.throws(() => nativeDriver.assessRecovery(r.state, r.replay, r.context, + r.c, r.plan, r.original, r.identity, pathApi), { code }) + }) + } + test(`${label} assessment rejects a sibling workspace and mismatched native workspace event`, () => { + const f = originalFixture(platformConfig(), id(901), pathApi) + f.run.workspace_path = pathApi.join(`${root}-sibling`, 'worktrees', + f.task.id.replaceAll('-', ''), f.run.id.replaceAll('-', '')) + assert.throws(() => nativeDriver.assessOriginal(f.state, f.replay, + f.c, f.plan, f.identity, pathApi), { code: 'original_workspace_lineage_mismatch' }) + const r = recoveryFixture(platformConfig(), { pathApi }) + const started = r.replay.events.find(entry => + entry.type === 'run.started' && entry.aggregate_id === r.replacement.id) + started.payload.workspace = pathApi.join(root, 'other-workspace') + assert.throws(() => nativeDriver.assessRecovery(r.state, r.replay, r.context, + r.c, r.plan, r.original, r.identity, pathApi), { code: 'native_verify_run_not_observed' }) + }) +} + +test('native assessment retains host-default path semantics when no override is supplied', () => { + const c = { ...config(), runner_root: path.sep === '\\' ? EXPECTED.runner_root : '/owned/runner-workspaces' } + const f = originalFixture(c, id(901), path) + assert.equal(nativeDriver.assessOriginal(f.state, f.replay, c, f.plan, f.identity).run_sha256, hashJson(f.run)) + const r = recoveryFixture(c, { approved: true, pathApi: path }) + assert.equal(nativeDriver.assessRecovery(r.state, r.replay, r.context, + c, r.plan, r.original, r.identity).verified, true) +}) + +test('suite path override rejects incompatible fixture paths before recovery', async () => { + const h = memoryHarness() + await assert.rejects(nativeDriver.executeSuite(h.c, h.report, h.io, { pathApi: path.posix }), + { code: 'unsafe_posix_path' }) + assert.deepEqual(h.mutations.map(entry => entry.operation), ['create']) + assert.equal(h.report.intents.recovery, null) + assert.equal(h.downloads.length, 0) +}) + for (const tokens of [5000, 6000]) { test(`assessOriginal binds native ${tokens === 5000 ? 'stop' : 'suspend'} from full replay, not snapshot.events`, () => { const fixture = originalFixture(config({ tokens })) @@ -1153,7 +1240,7 @@ function memoryHarness(c = config({ timeoutMs: 15_000, pollMs: 250, settleMs: 20 reads.base += 1 assert.deepEqual(identity, { task_id: original.task.id, run_id: original.run.id }) return { - path: path.join(original.run.workspace_path, 'base.txt'), bytes: 5, + path: WIN.join(original.run.workspace_path, 'base.txt'), bytes: 5, sha256: behavior.badBase ? '0'.repeat(64) : BASE_SHA, } }, diff --git a/tools/readiness_contract.test.mjs b/tools/readiness_contract.test.mjs new file mode 100644 index 00000000..f0760c92 --- /dev/null +++ b/tools/readiness_contract.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { selectNodeTests, checkPlan, summarizeTests, invocationFor } from './run_checks.mjs' +import { renderContract, replaceContract, START, END } from './check_docs.mjs' + +test('native test discovery covers existing web, tooling and steward suites without fixtures', () => { + assert.deepEqual(selectNodeTests(['tools/a.test.mjs', 'apps/web/src/b.test.mjs', 'scenarios/repo-steward/c.test.mjs', + 'tools/a.test.mjs', 'node_modules/d.test.mjs', 'tools/e2e_live.mjs', '../bad.test.mjs', 'tools/../bad.test.mjs']), + ['apps/web/src/b.test.mjs', 'scenarios/repo-steward/c.test.mjs', 'tools/a.test.mjs']) +}) +test('empty discovery, invalid config and unknown groups fail closed', () => { + assert.throws(() => checkPlan('node', []), /No Node test/) + assert.throws(() => checkPlan('made-up', ['tools/a.test.mjs']), /Unknown check/) + assert.throws(() => selectNodeTests([], { schemaVersion: 2 }), /Unsupported/) +}) +test('CommonJS CLI integration tests join the same native Node runner', () => { + assert.deepEqual(selectNodeTests(['tests/readiness/cli.test.js', 'tests/readiness/fixture.js']), ['tests/readiness/cli.test.js']) +}) +test('review skill package tests remain enrolled when the independently reviewed skill lands', () => { + const candidate = '.github/skills/code-review/tests/package.test.mjs' + assert.deepEqual(selectNodeTests([candidate, '.github/skills/code-review/upstream/fixture.js']), [candidate]) +}) +test('Windows package-manager execution uses an explicit argument vector, never a shell', () => { + assert.deepEqual(invocationFor('pnpm', ['build:web'], 'win32', 'C:\\Program Files\\pnpm\\bin\\pnpm.cjs'), + { program: process.execPath, args: ['C:\\Program Files\\pnpm\\bin\\pnpm.cjs', 'build:web'] }) + assert.deepEqual(invocationFor('pnpm', ['build:web'], 'win32', 'C:\\Program Files\\pnpm\\bin\\pnpm.mjs'), + { program: process.execPath, args: ['C:\\Program Files\\pnpm\\bin\\pnpm.mjs', 'build:web'] }) + assert.throws(() => invocationFor('pnpm', ['build:web'], 'win32', 'pnpm.cmd'), /native CLI path/) +}) +test('full gate retains Rust, web and migration checks and adds Node/docs validation', () => { + const plan = checkPlan('full', ['tools/a.test.mjs']) + assert.deepEqual(plan.map(check => check.name), ['migrations', 'docs', 'node-tests', 'format', 'clippy', 'rust-tests', 'web-build', 'web-lint']) + assert.deepEqual(plan.find(check => check.name === 'rust-tests').argv, ['cargo', 'test', '--workspace', '--locked']) +}) +test('test results distinguish failed, passed, ignored and not-observed', () => { + assert.deepEqual(summarizeTests('compile error'), { rust: null, node: null }) + const counts = summarizeTests('test result: ok. 2 passed; 0 failed; 3 ignored;\ntest result: FAILED. 1 passed; 2 failed; 1 ignored;\n# tests 9\n# pass 6\n# fail 1\n# skipped 2\n# todo 0\n') + assert.deepEqual(counts.rust, { passed: 3, failed: 2, ignored: 4, summaries: 2 }) + assert.deepEqual(counts.node, { tests: 9, passed: 6, failed: 1, skipped: 2, todo: 0, cancelled: null }) +}) +const pkg = { packageManager: 'pnpm@11.19.0', scripts: { test: 'cargo test', 'test:js': 'node --test', check: 'node check.mjs' } } +const settings = { nodeTestRoots: ['tools/'], rustCommand: ['cargo', 'test'], ignoredTests: 'Ignored is not passed.' } +test('generated docs follow canonical command and version changes', () => { + const rendered = renderContract(pkg, '24.19.0', '1.98.1', settings) + assert.match(rendered, /pnpm test:js/u) + assert.notEqual(rendered, renderContract({ ...pkg, scripts: { ...pkg.scripts, check: 'node new.mjs' } }, '24.19.0', '1.98.1', settings)) + assert.throws(() => renderContract(pkg, 'latest', '1.98.1', settings), /exact versions/) +}) +test('repair preserves authored prose, is idempotent, and refuses ambiguous blocks', () => { + const before = `Authored introduction\n${START}\nstale\n${END}\nHistorical evidence` + const generated = renderContract(pkg, '24.19.0', '1.98.1', settings) + const result = replaceContract(before, generated) + assert.ok(result.startsWith('Authored introduction\n')) + assert.ok(result.endsWith('\nHistorical evidence')) + assert.equal(replaceContract(result, generated), result) + for (const text of ['', `${START}${START}${END}`, `${END}${START}`]) assert.throws(() => replaceContract(text, generated), /Exactly one/) +}) +test('Windows CRLF checkout is not mistaken for documentation drift', () => { + const generated = renderContract(pkg, '24.19.0', '1.98.1', settings) + const document = `Intro\r\n${generated.replaceAll('\n', '\r\n')}\r\nEnd` + assert.equal(replaceContract(document, generated), document) +}) diff --git a/tools/run_checks.mjs b/tools/run_checks.mjs new file mode 100644 index 00000000..fd01e4b1 --- /dev/null +++ b/tools/run_checks.mjs @@ -0,0 +1,155 @@ +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const config = JSON.parse(readFileSync(new URL('../test.config.json', import.meta.url), 'utf8')) + +export function selectNodeTests(files, settings = config) { + if (settings.schemaVersion !== 1 || !Array.isArray(settings.nodeTestSuffixes) || !settings.nodeTestSuffixes.length || + settings.nodeTestSuffixes.some(suffix => !['.test.mjs', '.test.js'].includes(suffix)) || + !Array.isArray(settings.nodeTestRoots) || !settings.nodeTestRoots.length || + settings.nodeTestRoots.some(root => !/^[.\w/-]+\/$/u.test(root) || root.includes('..'))) { + throw new Error('Unsupported test configuration') + } + return [...new Set(files)].filter(file => !file.startsWith('-') && !file.includes('..') && + settings.nodeTestSuffixes.some(suffix => file.endsWith(suffix)) && settings.nodeTestRoots.some(root => file.startsWith(root))).sort() +} + +export function invocationFor(command, argv, platform = process.platform, pnpmPath = process.env.npm_execpath) { + if (command === 'node') return { program: process.execPath, args: argv } + if (command !== 'pnpm' || platform !== 'win32') return { program: command, args: argv } + if (!pnpmPath || !path.win32.isAbsolute(pnpmPath) || !/\.([cm]?js|exe)$/iu.test(pnpmPath)) { + throw new Error('Run the full Windows gate through pnpm check so its native CLI path is available') + } + return /\.exe$/iu.test(pnpmPath) ? { program: pnpmPath, args: argv } : { program: process.execPath, args: [pnpmPath, ...argv] } +} + +export function checkPlan(group, files) { + const tests = selectNodeTests(files) + if (!tests.length) throw new Error('No Node test files discovered; refusing a false-green suite') + if (JSON.stringify(config.rustCommand) !== JSON.stringify(['cargo', 'test', '--workspace', '--locked'])) { + throw new Error('Rust test command differs from the reviewed workspace gate') + } + const checks = { + migrations: ['node', 'tools/check_migrations.mjs'], + docs: ['node', 'tools/check_docs.mjs'], + 'node-tests': ['node', '--test', '--test-concurrency=1', '--test-reporter=tap', ...tests], + format: ['cargo', 'fmt', '--check'], + clippy: ['cargo', 'clippy', '--workspace', '--all-targets', '--locked', '--', '-D', 'warnings'], + 'rust-tests': config.rustCommand, + 'web-build': ['pnpm', 'build:web'], + 'web-lint': ['pnpm', 'lint:web'], + } + const groups = { + fast: ['migrations', 'docs', 'format'], + docs: ['docs'], + node: ['node-tests'], + test: ['node-tests', 'rust-tests'], + full: Object.keys(checks), + } + if (!Object.hasOwn(groups, group)) throw new Error('Unknown check group') + return groups[group].map(name => ({ name, argv: checks[name] })) +} + +export function summarizeTests(stdout) { + const rust = [...stdout.matchAll(/test result: \w+\. (\d+) passed; (\d+) failed; (\d+) ignored;/gu)] + const sum = index => rust.reduce((total, match) => total + Number(match[index]), 0) + const nodeValue = label => { + const match = stdout.match(new RegExp(`^# ${label} (\\d+)$`, 'm')) + return match ? Number(match[1]) : null + } + return { + rust: rust.length ? { passed: sum(1), failed: sum(2), ignored: sum(3), summaries: rust.length } : null, + node: nodeValue('tests') === null ? null : { + tests: nodeValue('tests'), passed: nodeValue('pass'), failed: nodeValue('fail'), + skipped: nodeValue('skipped'), todo: nodeValue('todo'), cancelled: nodeValue('cancelled'), + }, + } +} + +function git(args) { + const result = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }) + if (result.status !== 0) throw new Error(`Git evidence unavailable: ${args[0]}`) + return result.stdout +} + +export function main(args = process.argv.slice(2)) { + if (args.some(arg => !['--group', 'fast', 'docs', 'node', 'test', 'full', '--dry-run'].includes(arg)) || + args.filter(arg => arg === '--group').length !== 1 || args.indexOf('--group') !== 0 || + args.length < 2 || args.length > 3 || (args.length === 3 && args[2] !== '--dry-run')) { + throw new Error('Usage: node tools/run_checks.mjs --group fast|docs|node|test|full [--dry-run]') + } + const files = git(['ls-files', '-z', '--cached', '--others', '--exclude-standard']).split('\0').filter(Boolean) + const plan = checkPlan(args[1], files) + if (args.includes('--dry-run')) { + console.log(JSON.stringify({ dry_run: true, writes: [], checks: plan }, null, 2)) + return 0 + } + const status = git(['status', '--porcelain=v1']) + const changes = git(['diff', '--binary', 'HEAD']) + const untracked = git(['ls-files', '-z', '--others', '--exclude-standard']).split('\0').filter(Boolean) + const untrackedDigests = untracked.map(file => [file, createHash('sha256').update(readFileSync(path.join(ROOT, file))).digest('hex')]) + const started = new Date().toISOString() + const report = { + schemaVersion: 1, group: args[1], startedAt: started, + source: { commit: git(['rev-parse', 'HEAD']).trim(), branch: git(['branch', '--show-current']).trim(), + dirty: status.length > 0, trackedDiffSha256: createHash('sha256').update(changes).digest('hex'), untrackedDigests }, + node: process.versions.node, checks: [], status: 'running', + assurance: 'Local validation only. Ignored tests are not passes. No hosted CI, browser, provider or production claim.', + } + const out = path.join(ROOT, 'output', 'readiness') + mkdirSync(out, { recursive: true }) + const reportPath = path.join(out, `${started.replaceAll(/[:.]/gu, '-')}-${args[1]}.json`) + for (const check of plan) { + const begin = Date.now() + console.log(`Running ${check.name}`) + const [command, ...argv] = check.argv + // Use the package manager's own native entry point, not shell-concatenated arguments. + let result + try { + const invocation = invocationFor(command, argv) + result = spawnSync(invocation.program, invocation.args, { + cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, shell: false, + env: { ...process.env, ECORP_FACTORY_WATCH: '0' }, + }) + } catch (error) { + result = { status: null, error: { code: 'TOOL_RESOLUTION_ERROR' }, stdout: '', stderr: error.message + '\n' } + } + const stdout = result.stdout ?? '', stderr = result.stderr ?? '' + if (check.name === 'node-tests') { + // Hundreds of passing TAP records obscure actionable failures in agent context. + const failures = [...stdout.matchAll(/^\s*not ok .*$(?:\n[\s\S]*?^\s*\.\.\.)?/gmu)].map(match => match[0]) + const summary = stdout.split('\n').filter(line => /^# (tests|pass|fail|cancelled|skipped|todo|duration_ms) /u.test(line)) + console.log([...failures.map(value => value.slice(0, 2400)), ...summary].join('\n')) + } else if (check.name === 'rust-tests' && result.status === 0) { + console.log(stdout.split('\n').filter(line => line.startsWith('test result:')).join('\n')) + } else process.stdout.write(stdout) + process.stderr.write(stderr.slice(0, 16000)) + const counts = summarizeTests(stdout) + const testEvidence = check.name === 'node-tests' ? counts.node?.tests > 0 : + check.name === 'rust-tests' ? counts.rust?.summaries > 0 : true + const passed = result.status === 0 && !result.error && testEvidence + report.checks.push({ name: check.name, argv: check.argv, exitCode: result.status, + passed, durationMs: Date.now() - begin, counts, + errorCode: result.error?.code ?? (testEvidence ? null : 'NO_TEST_SUMMARY') }) + if (!passed) { report.status = 'failed'; break } + } + if (report.status === 'running') report.status = 'passed' + report.finishedAt = new Date().toISOString() + report.sourceChangedDuringValidation = git(['rev-parse', 'HEAD']).trim() !== report.source.commit || + createHash('sha256').update(git(['diff', '--binary', 'HEAD'])).digest('hex') !== report.source.trackedDiffSha256 || + JSON.stringify(git(['ls-files', '-z', '--others', '--exclude-standard']).split('\0').filter(Boolean) + .map(file => [file, createHash('sha256').update(readFileSync(path.join(ROOT, file))).digest('hex')])) !== JSON.stringify(untrackedDigests) + if (report.sourceChangedDuringValidation && report.status === 'passed') report.status = 'source_changed' + report.notRun = plan.slice(report.checks.length).map(check => check.name) + writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n', { flag: 'wx' }) + console.log(`Validation report: ${reportPath}`) + return report.status === 'passed' ? 0 : 1 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + try { process.exitCode = main() } catch (error) { console.error(error.message); process.exitCode = 1 } +} diff --git a/tools/security_canary.mjs b/tools/security_canary.mjs new file mode 100644 index 00000000..a80b1a62 --- /dev/null +++ b/tools/security_canary.mjs @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { randomBytes } from 'node:crypto' +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +// Detector verification, never a real credential or provider call. +const binary = process.argv[2] +if (!binary || !path.isAbsolute(binary) || process.argv.length !== 3) throw new Error('Supply one absolute Gitleaks executable path') +const config = fileURLToPath(new URL('../.gitleaks.toml', import.meta.url)) +const fixture = mkdtempSync(path.join(tmpdir(), 'ecorp-secret-canary-')) +const canary = ['gh', 'p_', randomBytes(27).toString('base64url').replaceAll('-', 'x').replaceAll('_', 'y')].join('') +try { + mkdirSync(path.join(fixture, 'docs/evidence'), { recursive: true }) + // A credential-shaped canary must still be caught inside an allowlisted artifact. + writeFileSync(path.join(fixture, 'docs/evidence/2026-09-08-room-context-browser.json'), JSON.stringify({ token: canary })) + const reportPath = path.join(fixture, 'report.json') + const result = spawnSync(binary, ['dir', fixture, '--config', config, '--redact=100', '--no-banner', '--report-format', 'json', '--report-path', reportPath], { encoding: 'utf8' }) + assert.equal(result.status, 1, 'Seeded invalid credential must make the detector fail') + const report = JSON.parse(readFileSync(reportPath, 'utf8')) + assert.ok(report.some(item => item.RuleID === 'github-pat')) + assert.ok(!JSON.stringify(report).includes(canary), 'Report must redact the seeded bytes') + console.log(JSON.stringify({ schemaVersion: 1, synthetic: true, detector: 'gitleaks', seeded_secret_rejected: true, evidence_exception_does_not_hide_credentials: true, report_redacted: true })) +} finally { + const relative = path.relative(tmpdir(), fixture) + assert.ok(relative.startsWith('ecorp-secret-canary-') && !relative.includes(path.sep)) + rmSync(fixture, { recursive: true }) +}