feat(ci): add global hash skip planning (#7185)

* feat(ci): add global hash skip planning

* refactor(ci): remove superseded hash skip cache

* refactor(ci): keep scope trust in planner

* fix(ci): run scope goldens for planner changes

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(ci): publish hash state after successful validation

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
This commit is contained in:
PerishFire
2026-08-21 00:12:33 +08:00
committed by GitHub
parent 4fccbdb31b
commit 84381efab9
28 changed files with 1586 additions and 3720 deletions
+24 -7
View File
@@ -11,8 +11,9 @@ Before changing GitHub automation, read the current versions of:
- `.github/workflows/autofix.atom.yml`
- `.github/workflows/report.atom.yml`
- `.github/scripts/handoff.py`
- `scripts/scopes.ts`
- `specs/current/ci.md` when changing scope rules, confidence tiers, or guards
- `.github/config/runners.json`, `.github/config/scopes.json`, and `.github/config/hash.json`
- `.github/scripts/runners.py`, `.github/scripts/scopes.py`, and `.github/scripts/hash.py`
- `specs/current/ci.md` when changing scope rules, confidence tiers, or planner invariants
- `e2e/tests/packaged-smoke-workflow.test.ts`
- `scripts/approve-fork-pr-workflows.ts` and `e2e/tests/scripts/approve-fork-pr-workflows.test.ts` when touching fork PR approval behavior
@@ -26,7 +27,7 @@ Business layer:
- Business workflows decide what happened and what should be requested next.
- `ci.yml` is the main low-privilege PR, merge-queue, and manual validation gate (application merge bar only).
- `ci.yml` should run validation, decide scopes, and produce typed handoff artifacts.
- `ci.yml` should resolve runners, compose scope and hash decisions in its Linux `plan` job, run validation, and produce typed handoff artifacts.
- Packaging checks are standalone and outside the merge gate: `nix.yml` (flake check) and `docker-image.yml` (image validate + publish). Do not re-attach them to `Validate workspace`.
- Business workflows should not perform trusted writes to PR comments or branches when a capability workflow can do it.
@@ -51,6 +52,21 @@ Default rule: do not add a new domain-specific follow-on workflow such as `foo.c
New workflow-owned helpers should usually live under `.github/scripts/`. Prefer TypeScript for project-owned scripts in general, but Python is acceptable for small GitHub runner glue when stdlib portability and low setup cost matter. Keep such exceptions narrow and covered by `pnpm guard` policy.
The CI control plane is deliberately Linux-only and stdlib-only. Runner classes,
scope rules, and hash declarations live in `.github/config/`; their Python
entrypoints initialize metadata before workload runners start. A Windows job
must never invoke these scripts. Keep the four layers independent: runner
placement, changed-file scopes, input hashes, and fine-grained commands inside
a workload.
`hash.py` is a static comparison register, not a success cache. It reads the
previous identity-to-hash map restored by Actions cache, computes the current
map from Git inputs, and replaces the local state immediately. The plan carries
that pending map to `validate`, which publishes it only after the gate succeeds;
a failed run therefore cannot authorize identical-input skips on a fresh retry.
Only a workload's YAML `if` gives the comparison skip semantics; cache loss or
corruption starts cold.
## Handoff contract
Use `.github/scripts/handoff.py` for all CI follow-on artifact names and paths. The canonical layout is:
@@ -125,13 +141,14 @@ Keep `.github/workflows/ci.yml` as the only approved workflow path unless a main
- Same-repo patch: produce `handoff/autofix` and let `autofix.atom.yml` consume it.
- Rich/generated comment: produce `handoff/report` and let `report.atom.yml` materialize and upsert it.
- New naming, paths, or metadata: update `.github/scripts/handoff.py`.
2. Update scope routing in `scripts/scopes.ts` when a workflow/script should trigger a validation lane.
3. Update topology coverage in `e2e/tests/packaged-smoke-workflow.test.ts` or the relevant script test.
4. Run the focused checks:
2. Update scope routing in `.github/config/scopes.json`, then run `python3 .github/scripts/scopes.py validate`.
3. Declare workload input closure in `.github/config/hash.json`; use `"*"` until a narrower set has high-confidence evidence.
4. Update topology coverage in `e2e/tests/packaged-smoke-workflow.test.ts` or the relevant script test.
5. Run the focused checks:
- `python3 .github/scripts/handoff.py self-check`
- `actionlint -color`
- `pnpm --filter @open-design/e2e test tests/packaged-smoke-workflow.test.ts`
5. Run repo-level checks before handing off:
6. Run repo-level checks before handing off:
- `pnpm guard`
- `pnpm typecheck`
-44
View File
@@ -1,44 +0,0 @@
name: Hash skip
description: Skip a caller-defined unit when its declared input hash has already completed successfully
inputs:
hash_skip:
description: Declaration key under .github/hash_skip
required: true
mode:
description: normal, verify, quarantine, or complete
required: false
default: normal
outputs:
hash:
value: ${{ steps.resolve.outputs.hash }}
reason:
value: ${{ inputs.mode == 'complete' && 'complete' || inputs.mode == 'quarantine' && 'quarantine' || inputs.mode == 'verify' && 'verify' || steps.restore.outputs.cache-hit == 'true' && 'hit' || 'miss' }}
run:
value: ${{ inputs.mode != 'complete' && (inputs.mode != 'normal' || steps.restore.outputs.cache-hit != 'true') }}
skip:
value: ${{ inputs.mode == 'normal' && steps.restore.outputs.cache-hit == 'true' }}
runs:
using: composite
steps:
- name: Resolve declared hash
id: resolve
shell: bash
run: node --experimental-strip-types .github/scripts/hash_skip.ts "${{ inputs.hash_skip }}" --mode "${{ inputs.mode }}"
- name: Resolve successful execution cache
id: restore
if: ${{ inputs.mode != 'quarantine' && inputs.mode != 'complete' }}
uses: actions/cache/restore@v5.0.5
with:
enableCrossOsArchive: true
key: ${{ steps.resolve.outputs.cache_key }}
path: ${{ steps.resolve.outputs.marker }}
- name: Register successful execution cache
if: ${{ inputs.mode == 'complete' }}
uses: actions/cache/save@v5.0.5
with:
enableCrossOsArchive: true
key: ${{ steps.resolve.outputs.cache_key }}
path: ${{ steps.resolve.outputs.marker }}
+41
View File
@@ -0,0 +1,41 @@
{
"schema": { "version": 1 },
"suites": {
"ci-control": [
".github/config/hash.json",
".github/config/scopes.json",
".github/scripts/hash.py",
".github/scripts/scopes.py",
".github/scripts/lib/",
".github/workflows/ci.yml"
],
"web": [
"apps/web/", "packages/components/", "packages/contracts/", "packages/host/",
"packages/platform/", "packages/release/", "packages/sidecar/", "packages/sidecar-proto/",
"package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"
],
"tools-pack": [
"tools/pack/", "apps/packaged/", "apps/desktop/", "packages/components/", "packages/host/",
"packages/platform/", "packages/release/", "packages/sidecar/", "packages/sidecar-proto/",
"package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"
],
"ui": [
"suite://web", "apps/daemon/", "e2e/", ".github/actions/setup-playwright/",
".github/actions/setup-workspace/"
]
},
"workflows": {
"ci": {
"static_gate": ["*"],
"preflight": ["*"],
"workspace_unit_tests": ["*"],
"daemon_unit_tests": ["*"],
"windows_tools_pack_payload_tests": ["*"],
"web_workspace_tests": ["*"],
"e2e_vitest": ["*"],
"playwright_critical": ["*"],
"ui_p0": ["*"],
"playwright_visual": ["*"]
}
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"schema": { "version": 1 },
"defaultMode": "default",
"aliases": { "performance": "default" },
"profiles": {
"github.ubuntu": ["ubuntu-24.04"],
"github.windows": ["windows-latest"],
"blacksmith.small": ["blacksmith-4vcpu-ubuntu-2404"],
"blacksmith.large": ["blacksmith-8vcpu-ubuntu-2404"],
"nexu.small": ["nexu-runners-small"],
"nexu.medium": ["nexu-runners-medium"],
"nexu.large": ["nexu-runners-large"],
"nexu.xlarge": ["nexu-runners-xlarge"]
},
"modes": {
"default": {
"control": "nexu.small",
"general_medium": "nexu.medium",
"workspace_unit": "nexu.medium",
"windows_tools": "github.windows",
"js_hot": "nexu.medium",
"ui_hot": "nexu.large",
"ui_p0": "nexu.medium",
"ui_p0_heavy": "nexu.xlarge",
"visual_hot": "nexu.large"
},
"economic": {
"control": "github.ubuntu",
"general_medium": "github.ubuntu",
"workspace_unit": "github.ubuntu",
"windows_tools": "github.windows",
"js_hot": "github.ubuntu",
"ui_hot": "github.ubuntu",
"ui_p0": "github.ubuntu",
"ui_p0_heavy": "github.ubuntu",
"visual_hot": "github.ubuntu"
},
"blacksmith": {
"control": "blacksmith.small",
"general_medium": "blacksmith.small",
"workspace_unit": "blacksmith.small",
"windows_tools": "github.windows",
"js_hot": "blacksmith.small",
"ui_hot": "blacksmith.large",
"ui_p0": "blacksmith.large",
"ui_p0_heavy": "blacksmith.large",
"visual_hot": "blacksmith.large"
}
}
}
+199
View File
@@ -0,0 +1,199 @@
{
"schema": { "version": 1 },
"effects": [
"daemon_tests_required",
"web_tests_required",
"tools_dev_tests_required",
"tools_pack_tests_required",
"ui_critical_validation_required",
"ui_p0_validation_required",
"visual_validation_required",
"workspace_validation_required"
],
"matches": {
"daemon-runtime-definition": {
"prefixes": ["apps/daemon/src/runtimes/defs/"],
"exact": [
"apps/daemon/src/runtimes/capabilities.ts",
"apps/daemon/src/runtimes/local-profiles.ts",
"apps/daemon/src/runtimes/metadata.ts",
"apps/daemon/src/runtimes/registry.ts",
"apps/daemon/tests/runtimes/agent-args.test.ts",
"apps/daemon/tests/runtimes/antigravity-model-lock.test.ts",
"apps/daemon/tests/runtimes/atomcode.test.ts",
"apps/daemon/tests/runtimes/byok-opencode.test.ts",
"apps/daemon/tests/runtimes/chat-run-inactivity-timeout.test.ts",
"apps/daemon/tests/runtimes/claude-resume-args.test.ts",
"apps/daemon/tests/runtimes/codebuddy.test.ts",
"apps/daemon/tests/runtimes/codex-resume-args.test.ts",
"apps/daemon/tests/runtimes/detection-resilience.test.ts",
"apps/daemon/tests/runtimes/opencode-resume-args.test.ts",
"apps/daemon/tests/runtimes/registry-and-args.test.ts",
"apps/daemon/tests/runtimes/trae-cli.test.ts"
]
},
"certain-exempt": {
"prefixes": [".vscode/", ".idea/", "docs/", "apps/landing-page/", ".github/ISSUE_TEMPLATE/"],
"exact": ["LICENSE", ".github/CODEOWNERS"],
"exclude": ["match://daemon-doc"]
},
"daemon-doc": { "exact": ["docs/agent-adapters.md"] },
"packaged-leaf": {
"prefixes": [
"apps/desktop/src/", "apps/desktop/tests/", "apps/packaged/src/", "apps/packaged/tests/",
"tools/pack/src/", "tools/pack/tests/", "tools/pack/resources/"
]
},
"daemon-core": {
"prefixes": ["apps/daemon/src/", "apps/daemon/tests/"],
"exact": ["docs/agent-adapters.md"],
"exclude": ["match://daemon-runtime-definition", "prefix://apps/daemon/src/sidecar/"]
},
"medium-exempt": {
"prefixes": ["nix/"],
"exact": [
".gitignore", ".editorconfig", "flake.nix", "flake.lock",
".github/workflows/landing-page-ci.yml", ".github/workflows/landing-page-staging.yml",
".github/workflows/landing-page-production.yml", ".github/workflows/blog-indexing-on-deploy.yml",
".github/workflows/autofix.atom.yml", ".github/workflows/comment.atom.yml",
".github/workflows/report.atom.yml", ".github/workflows/docker-image.yml", ".github/workflows/nix.yml"
],
"regexes": ["\\.(?:md|mdx|txt)$"],
"exclude": ["match://certain-surface"]
},
"certain-surface": {
"include": ["match://certain-exempt", "match://packaged-leaf", "match://daemon-core"]
},
"workspace-exempt": {
"include": ["match://certain-exempt", "match://medium-exempt", "match://packaged-leaf", "match://daemon-core"]
},
"ui-critical-exempt": {
"include": ["match://certain-exempt", "match://medium-exempt", "match://daemon-core"],
"prefixes": ["apps/desktop/", "apps/packaged/", "tools/pack/"]
}
},
"rules": [
{
"id": "certain-exempt-surface", "match": { "include": ["match://certain-exempt"] },
"effects": [], "confidence": "certain"
},
{
"id": "exempt-surface", "match": { "include": ["match://medium-exempt"] },
"effects": [], "confidence": "medium"
},
{
"id": "certain-daemon-core", "match": { "include": ["match://daemon-core"] },
"effects": ["daemon_tests_required", "ui_critical_validation_required", "ui_p0_validation_required", "workspace_validation_required"],
"confidence": "certain"
},
{
"id": "daemon-sources",
"match": {
"prefixes": ["apps/daemon/", "packages/release/", "packages/contracts/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/"],
"exclude": ["match://daemon-core"]
},
"effects": ["daemon_tests_required"], "confidence": "medium"
},
{
"id": "web-sources",
"match": { "prefixes": ["apps/web/", "packages/release/", "packages/components/", "packages/contracts/", "packages/host/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/"] },
"effects": ["web_tests_required"], "confidence": "medium"
},
{
"id": "runtime-content",
"match": { "prefixes": ["scripts/", "assets/", "skills/", "prompt-templates/", "design-systems/", "design-templates/", "craft/"] },
"effects": ["daemon_tests_required", "web_tests_required"], "confidence": "medium"
},
{
"id": "script-contract-tests", "match": { "prefixes": ["e2e/tests/scripts/"] },
"effects": ["daemon_tests_required", "web_tests_required"], "confidence": "medium"
},
{
"id": "tools-dev-sources",
"match": { "prefixes": ["tools/dev/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/"] },
"effects": ["tools_dev_tests_required"], "confidence": "medium"
},
{
"id": "certain-packaged-leaf-sources", "match": { "include": ["match://packaged-leaf"] },
"effects": ["tools_dev_tests_required", "tools_pack_tests_required", "workspace_validation_required"],
"confidence": "certain"
},
{
"id": "tools-pack-sources",
"match": {
"prefixes": ["tools/pack/", "apps/packaged/", "apps/desktop/", "packages/release/", "packages/components/", "packages/host/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/"],
"exclude": ["match://packaged-leaf"]
},
"effects": ["tools_pack_tests_required"], "confidence": "medium"
},
{
"id": "workspace-manifests-and-ci",
"match": {
"exact": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", ".github/workflows/ci.yml", "e2e/package.json"],
"regexes": ["^apps/[^/]+/package\\.json$", "^packages/[^/]+/package\\.json$", "^tools/[^/]+/package\\.json$"]
},
"effects": ["daemon_tests_required", "web_tests_required", "tools_dev_tests_required", "tools_pack_tests_required"],
"confidence": "medium"
},
{
"id": "ui-p0-surface",
"match": {
"prefixes": ["apps/web/", "apps/daemon/", "packages/release/", "packages/components/", "packages/contracts/", "packages/host/", "packages/platform/", "packages/sidecar/", "packages/sidecar-proto/", "e2e/ui/", "e2e/lib/", "e2e/resources/", "e2e/scripts/", ".github/actions/setup-playwright/", ".github/actions/setup-workspace/"],
"exact": ["e2e/package.json", "e2e/playwright.config.ts", "package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", ".github/workflows/ci.yml", ".github/workflows/ui-extended-main.yml"],
"exclude": ["match://daemon-core"]
},
"effects": ["ui_p0_validation_required"], "confidence": "medium"
},
{
"id": "visual-surface",
"match": {
"prefixes": ["apps/web/", "e2e/lib/playwright/", ".github/actions/setup-playwright/", ".github/actions/setup-workspace/"],
"exact": ["e2e/package.json", "e2e/playwright.visual.config.ts", "e2e/scripts/playwright.ts", "e2e/scripts/visual-report.ts", "pnpm-lock.yaml", ".github/scripts/handoff.py", ".github/workflows/ci.yml", ".github/workflows/comment.atom.yml", ".github/workflows/report.atom.yml", ".github/workflows/visual-baseline.yml"],
"regexes": ["^e2e/ui/visual-[^/]+\\.test\\.ts$"]
},
"effects": ["visual_validation_required"], "confidence": "medium"
},
{
"id": "ci-control-plane",
"match": {
"prefixes": [".github/config/"],
"exact": [
".github/scripts/scopes.py", ".github/scripts/hash.py", ".github/scripts/runners.py",
".github/scripts/lib/config.py", ".github/scripts/lib/github.py"
]
},
"effects": ["web_tests_required", "workspace_validation_required"], "confidence": "medium"
},
{
"id": "ci-rerun-infra-cancel-surface",
"match": { "exact": [".github/workflows/rerun.atom.yml", ".github/scripts/rerun_infra_cancel.py", "e2e/tests/packaged-smoke-workflow.test.ts"] },
"effects": ["web_tests_required", "workspace_validation_required"], "confidence": "medium"
},
{
"id": "workspace-fallback", "match": { "exclude": ["match://workspace-exempt"] },
"effects": ["workspace_validation_required"], "confidence": "medium"
},
{
"id": "ui-critical-fallback", "match": { "exclude": ["match://ui-critical-exempt"] },
"effects": ["ui_critical_validation_required"], "confidence": "medium"
}
],
"matrices": {
"ui_p0": [
{ "name": "entry-settings", "shard": "entry-settings" },
{ "name": "project-workspace", "shard": "project-workspace" },
{ "name": "project-workspace-editor", "shard": "project-workspace-editor" },
{ "name": "project-collab", "shard": "project-collab" },
{ "name": "project-runtime", "shard": "project-runtime" },
{ "name": "workspace-restoration", "shard": "workspace-restoration" }
],
"visual": [
{ "name": "entry-navigation", "files": "ui/visual-entry.test.ts ui/visual-navigation.test.ts" },
{ "name": "settings-workspace", "files": "ui/visual-settings.test.ts ui/visual-workspace.test.ts" }
]
},
"uiP0Shadow": {
"match": "daemon-runtime-definition",
"matrixNames": ["entry-settings", "project-workspace", "project-collab", "project-runtime"]
}
}
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path, PurePosixPath
from lib.config import ConfigError, compact_json, load_json, object_value, repository_root, schema_v1
from lib.github import append_outputs, append_summary
PROTOCOL = "nexu-hash-v1"
CONTROL_SUITE = "ci-control"
class HashContract:
def __init__(self, path):
value = object_value(load_json(path), "hash")
if set(value) != {"schema", "suites", "workflows"}:
raise ConfigError("hash keys must be schema, suites, and workflows")
schema_v1(value, "hash")
self.suites = object_value(value["suites"], "hash.suites")
self.workflows = object_value(value["workflows"], "hash.workflows")
if CONTROL_SUITE not in self.suites:
raise ConfigError(f"hash.suites must define {CONTROL_SUITE}")
self._validate()
@staticmethod
def _tokens(value, label):
if not isinstance(value, list) or not value:
raise ConfigError(f"{label} must be a non-empty array")
if any(not isinstance(token, str) or not token for token in value):
raise ConfigError(f"{label} contains an invalid token")
return value
@staticmethod
def _validate_path(token, label):
if token == "*":
return
if token.startswith(("/", "~")) or "\\" in token or "\n" in token:
raise ConfigError(f"{label} has unsafe path token {token!r}")
if ".." in PurePosixPath(token).parts:
raise ConfigError(f"{label} escapes the repository: {token!r}")
if "://" in token:
raise ConfigError(f"{label} has unsupported token scheme: {token}")
def _validate(self):
nodes = {}
for suite, raw in self.suites.items():
if not isinstance(suite, str) or not suite:
raise ConfigError("hash.suites keys must be non-empty strings")
nodes[f"suite://{suite}"] = self._tokens(raw, f"hash.suites.{suite}")
for workflow, identities in self.workflows.items():
if not isinstance(workflow, str) or not workflow or "/" in workflow:
raise ConfigError("hash.workflows keys must be non-empty strings")
identities = object_value(identities, f"hash.workflows.{workflow}")
if not identities:
raise ConfigError(f"hash.workflows.{workflow} must not be empty")
for identity, raw in identities.items():
if not isinstance(identity, str) or not identity or "/" in identity:
raise ConfigError(f"hash.workflows.{workflow} has an invalid identity")
nodes[f"key://{workflow}/{identity}"] = self._tokens(raw, f"hash.workflows.{workflow}.{identity}")
for node, tokens in nodes.items():
for token in tokens:
if token.startswith(("suite://", "key://")):
if token not in nodes:
raise ConfigError(f"{node} references unknown {token}")
else:
self._validate_path(token, node)
visiting, complete = [], set()
def visit(node):
if node in visiting:
raise ConfigError(f"hash dependency cycle: {' -> '.join((*visiting, node))}")
if node in complete:
return
visiting.append(node)
tokens = nodes[node]
if node.startswith("key://"):
tokens = [f"suite://{CONTROL_SUITE}", *tokens]
for token in tokens:
if token in nodes:
visit(token)
visiting.pop()
complete.add(node)
for node in nodes:
visit(node)
def declarations(self, workflow):
if workflow not in self.workflows:
raise ConfigError(f"unknown hash workflow: {workflow}")
return self.workflows[workflow]
class GitFingerprinter:
def __init__(self, root):
self.root = root
self.cache = {}
def records(self, token):
if token in self.cache:
return self.cache[token]
if token == "*":
pathspec = []
elif any(character in token for character in "*?["):
pathspec = [f":(glob){token}"]
else:
pathspec = [token]
command = ["git", "ls-files", "-s", "-z"]
if pathspec:
command += ["--", *pathspec]
result = subprocess.run(command, cwd=self.root, check=True, stdout=subprocess.PIPE)
records = []
for raw in result.stdout.split(b"\0"):
if not raw:
continue
metadata, path = raw.split(b"\t", 1)
mode, oid, stage = metadata.decode("ascii").split()
records.append((path.decode("utf-8", "surrogateescape"), mode, oid, stage))
records.sort()
if not records:
raise ConfigError(f"hash path token matched no tracked files: {token}")
self.cache[token] = records
return records
def digest_node(contract, fingerprinter, node, tokens, resolved):
if node in resolved:
return resolved[node]
if node.startswith("key://"):
tokens = [f"suite://{CONTROL_SUITE}", *tokens]
digest = hashlib.sha256()
digest.update(f"{PROTOCOL}\0{node}\0".encode())
for token in tokens:
digest.update(f"token\0{token}\0".encode())
if token.startswith("suite://"):
name = token.removeprefix("suite://")
child = digest_node(contract, fingerprinter, token, contract.suites[name], resolved)
digest.update(f"digest\0{child}\0".encode())
elif token.startswith("key://"):
workflow, identity = token.removeprefix("key://").split("/", 1)
child = digest_node(contract, fingerprinter, token, contract.workflows[workflow][identity], resolved)
digest.update(f"digest\0{child}\0".encode())
else:
for path, mode, oid, stage in fingerprinter.records(token):
digest.update(f"file\0{path}\0{mode}\0{oid}\0{stage}\0".encode("utf-8", "surrogateescape"))
resolved[node] = digest.hexdigest()
return resolved[node]
def calculate(contract, root, workflow):
resolved = {}
fingerprinter = GitFingerprinter(root)
hashes = {}
for identity, declared in contract.declarations(workflow).items():
node = f"key://{workflow}/{identity}"
# Control inputs are implicit so later optimized declarations cannot
# accidentally make their own planner/configuration changes invisible.
hashes[identity] = digest_node(contract, fingerprinter, node, declared, resolved)
return hashes
def read_previous(path, workflow):
try:
value = load_json(path)
if value.get("schemaVersion") != 1 or value.get("workflow") != workflow or not isinstance(value.get("hashes"), dict):
raise ConfigError("state contract differs")
if any(not isinstance(key, str) or not isinstance(digest, str) for key, digest in value["hashes"].items()):
raise ConfigError("state hashes are invalid")
return value["hashes"], None
except (ConfigError, AttributeError) as error:
return {}, str(error)
def write_state(path, workflow, hashes):
path.parent.mkdir(parents=True, exist_ok=True)
payload = {"schemaVersion": 1, "workflow": workflow, "hashes": hashes}
handle, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent), text=True)
try:
with os.fdopen(handle, "w", encoding="utf-8") as output:
json.dump(payload, output, indent=2, sort_keys=True)
output.write("\n")
os.replace(temporary, path)
except BaseException:
try:
os.unlink(temporary)
except OSError:
pass
raise
def execute(root, contract, workflow, scope_plan_path, state_path):
plan = load_json(scope_plan_path)
enabled = object_value(plan.get("enabled"), "scope plan.enabled")
identities = set(contract.declarations(workflow))
if set(enabled) != identities:
raise ConfigError(f"scope/hash identity mismatch (scope={sorted(enabled)}, hash={sorted(identities)})")
if any(not isinstance(value, bool) for value in enabled.values()):
raise ConfigError("scope plan.enabled values must be booleans")
previous, state_warning = read_previous(state_path, workflow) if state_path.exists() else ({}, "state missing")
current = calculate(contract, root, workflow)
equal = {identity: previous.get(identity) == digest for identity, digest in current.items()}
run = {identity: bool(enabled[identity]) and not equal[identity] for identity in current}
reasons = {
identity: "scope-disabled" if not enabled[identity] else "hash-equal" if equal[identity] else "hash-changed"
for identity in current
}
write_state(state_path, workflow, current)
append_outputs({"run": compact_json(run), "equal": compact_json(equal)})
lines = ["### Hash decisions", "", "| Identity | Scope | Equal | Run | Reason |", "| --- | ---: | ---: | ---: | --- |"]
for identity in current:
lines.append(f"| {identity} | {str(bool(enabled[identity])).lower()} | {str(equal[identity]).lower()} | {str(run[identity]).lower()} | {reasons[identity]} |")
if state_warning:
lines += ["", f"> Previous state unavailable ({state_warning}); identities start cold."]
append_summary("\n".join(lines))
print(json.dumps({"run": run, "equal": equal, "reasons": reasons}, indent=2, sort_keys=True))
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path)
parser.add_argument("--root", type=Path)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("validate")
github = sub.add_parser("github-output")
github.add_argument("--workflow", required=True)
github.add_argument("--scope-plan", type=Path, required=True)
github.add_argument("--state", type=Path, required=True)
return parser.parse_args()
def main():
args = parse_args()
root = args.root.resolve() if args.root else repository_root(__file__)
contract = HashContract(args.config or root / ".github/config/hash.json")
if args.command == "validate":
for workflow in contract.workflows:
calculate(contract, root, workflow)
print("hash configuration is valid")
return 0
execute(root, contract, args.workflow, args.scope_plan, args.state)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (ConfigError, subprocess.SubprocessError, OSError) as error:
print(f"hash configuration error: {error}", file=sys.stderr)
raise SystemExit(2)
-140
View File
@@ -1,140 +0,0 @@
#!/usr/bin/env -S node --experimental-strip-types
import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { execFileSync } from "node:child_process";
import { ensure, fail } from "./lib/control.ts";
import { git, gitPathSetDigest, type GitHashPath } from "./lib/git.ts";
import { canonicalJson, digest, type Digest } from "./lib/json.ts";
type Declaration = Readonly<{
extraDigests: readonly string[];
hashPaths: readonly GitHashPath[];
schemaVersion: number;
}>;
const KEY_PATTERN = /^[a-z][a-z0-9.-]*$/u;
const EXTRA_PATTERN = /^[a-z][a-z0-9]*$/u;
const CONTROL_PATHS = [
".github/actions/hash-skip/action.yml",
".github/scripts/hash_skip.ts",
".github/scripts/lib/control.ts",
".github/scripts/lib/git.ts",
".github/scripts/lib/json.ts",
] as const;
function repoPath(value: unknown, label: string): string {
const text = ensure.text(value, label).replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
if (isAbsolute(text) || text === ".." || text.startsWith("../") || text.includes("/../")) {
fail(`${label} must be repository-relative`);
}
return text;
}
function strings(value: unknown, label: string): readonly string[] {
const entries = ensure.array(value, label).map((entry, index) => ensure.text(entry, `${label}[${index}]`)).sort();
ensure.that(new Set(entries).size === entries.length, `${label} must not contain duplicates`);
return Object.freeze(entries);
}
function hashPath(value: unknown, label: string): GitHashPath {
if (typeof value === "string") return Object.freeze({ path: repoPath(value, label) });
const input = ensure.record(value, label);
ensure.exactKeys(input, ["excludeDirectoryNames", "excludePaths", "normalizePackageVersion", "normalizeTextLineEndings", "path"], label);
for (const field of ["normalizePackageVersion", "normalizeTextLineEndings"] as const) {
ensure.that(input[field] == null || typeof input[field] === "boolean", `${label}.${field} must be boolean`);
}
return Object.freeze({
...(input.excludeDirectoryNames == null ? {} : { excludeDirectoryNames: strings(input.excludeDirectoryNames, `${label}.excludeDirectoryNames`) }),
...(input.excludePaths == null ? {} : { excludePaths: strings(input.excludePaths, `${label}.excludePaths`).map((entry) => repoPath(entry, `${label}.excludePaths`)) }),
...(input.normalizePackageVersion === true ? { normalizePackageVersion: true } : {}),
...(input.normalizeTextLineEndings === true ? { normalizeTextLineEndings: true } : {}),
path: repoPath(input.path, `${label}.path`),
});
}
function declaration(root: string, key: string, ref: string): Declaration {
ensure.that(KEY_PATTERN.test(key), `invalid hash_skip key: ${key}`);
const path = `.github/hash_skip/${key}.json`;
let value: unknown;
try { value = JSON.parse(git(["show", `${ref}:${path}`], root).toString("utf8")); } catch (error) {
fail(`invalid hash_skip declaration ${key}: ${error instanceof Error ? error.message : String(error)}`);
}
const input = ensure.record(value, `hash_skip ${key}`);
ensure.exactKeys(input, ["extraDigests", "hashPaths", "schemaVersion"], `hash_skip ${key}`);
const extraDigests = strings(input.extraDigests, `hash_skip ${key}.extraDigests`);
ensure.that(extraDigests.every((name) => EXTRA_PATTERN.test(name)), `hash_skip ${key}.extraDigests contains an invalid name`);
const hashPaths = ensure.array(input.hashPaths, `hash_skip ${key}.hashPaths`).map((entry, index) => hashPath(entry, `hash_skip ${key}.hashPaths[${index}]`));
ensure.that(hashPaths.length > 0, `hash_skip ${key}.hashPaths must not be empty`);
return Object.freeze({ extraDigests, hashPaths: Object.freeze(hashPaths), schemaVersion: ensure.integer(input.schemaVersion, `hash_skip ${key}.schemaVersion`) });
}
function extraDigests(definition: Declaration): Readonly<Record<string, Digest>> {
return Object.freeze(Object.fromEntries(definition.extraDigests.map((name) => {
const env = `HASH_SKIP_EXTRA_${name.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase()}`;
return [name, ensure.digest(process.env[env], env)];
})));
}
function resolveKey(root: string, key: string, ref: string, mode = "normal"): Readonly<{ cacheKey: string; hash: Digest; marker: string }> {
ensure.that(["complete", "normal", "quarantine", "verify"].includes(mode), "hash_skip mode must be complete, normal, quarantine, or verify");
const commit = git(["rev-parse", "--verify", `${ref}^{commit}`], root).toString("utf8").trim();
const definition = declaration(root, key, commit);
const source = gitPathSetDigest(root, commit, definition.hashPaths, { domain: "open-design/hash-skip/source/v1", label: key });
const control = gitPathSetDigest(root, commit, CONTROL_PATHS.map((path) => ({ path })), { domain: "open-design/hash-skip/control/v1", label: "hash_skip control" });
const hash = digest(canonicalJson({ control, definition, domain: "open-design/hash-skip/v1", extras: extraDigests(definition), key, source }));
const token = hash.slice("sha256:".length);
const marker = `.tmp/hash_skip/${token}/success.json`;
const absoluteMarker = join(root, marker);
mkdirSync(dirname(absoluteMarker), { recursive: true });
writeFileSync(absoluteMarker, `${JSON.stringify({ hash, key, schemaVersion: 1 })}\n`);
return Object.freeze({ cacheKey: `hash-skip-v1-${key}-${token}`, hash, marker });
}
function emit(result: ReturnType<typeof resolveKey>): void {
const output = process.env.GITHUB_OUTPUT;
if (output == null) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
else appendFileSync(output, `cache_key=${result.cacheKey}\nhash=${result.hash}\nmarker=${result.marker}\n`);
}
function init(root: string): void {
execFileSync("git", ["init", "-q"], { cwd: root });
execFileSync("git", ["config", "user.email", "hash-skip@open-design.invalid"], { cwd: root });
execFileSync("git", ["config", "user.name", "hash_skip self-check"], { cwd: root });
}
function selfCheck(): void {
const root = mkdtempSync(join(tmpdir(), "open-design-hash-skip-"));
try {
init(root);
for (const path of CONTROL_PATHS) { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), `${path}\n`); }
mkdirSync(join(root, ".github", "hash_skip"), { recursive: true });
writeFileSync(join(root, ".github", "hash_skip", "probe.json"), JSON.stringify({ extraDigests: ["runtime"], hashPaths: ["source.txt"], schemaVersion: 1 }));
writeFileSync(join(root, "source.txt"), "one\n");
execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-qm", "one"], { cwd: root });
process.env.HASH_SKIP_EXTRA_RUNTIME = digest("node24");
const first = resolveKey(root, "probe", "HEAD");
ensure.that(first.marker.startsWith(".tmp/hash_skip/") && !isAbsolute(first.marker), "cache marker must be repository-relative and cross-runner portable");
ensure.that(first.hash === resolveKey(root, "probe", "HEAD").hash, "same inputs produced different hashes");
const completed = resolveKey(root, "probe", "HEAD", "complete");
ensure.that(first.hash === completed.hash && first.cacheKey === completed.cacheKey && first.marker === completed.marker, "normal and complete modes produced asymmetric identities");
writeFileSync(join(root, "source.txt"), "two\n"); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-qm", "two"], { cwd: root });
const sourceChanged = resolveKey(root, "probe", "HEAD");
ensure.that(first.hash !== sourceChanged.hash, "source change did not invalidate hash");
process.env.HASH_SKIP_EXTRA_RUNTIME = digest("node25");
ensure.that(sourceChanged.hash !== resolveKey(root, "probe", "HEAD").hash, "extra digest change did not invalidate hash");
process.stdout.write("hash_skip self-check OK\n");
} finally { rmSync(root, { force: true, recursive: true }); }
}
const [key, ...args] = process.argv.slice(2);
if (key == null || key === "--help") process.stdout.write("Usage: hash_skip.ts <key> [--root <path>] [--ref <git-ref>] | self-check\n");
else if (key === "self-check") selfCheck();
else {
ensure.that(args.length % 2 === 0, "hash_skip options must be pairs");
const options = Object.fromEntries(Array.from({ length: args.length / 2 }, (_, index) => [args[index * 2]!.replace(/^--/u, ""), args[index * 2 + 1]!]));
ensure.exactKeys(options, ["mode", "ref", "root"], "hash_skip options");
emit(resolveKey(resolve(options.root ?? process.cwd()), key, options.ref ?? "HEAD", options.mode ?? "normal"));
}
+55
View File
@@ -0,0 +1,55 @@
import json
from pathlib import Path
class ConfigError(ValueError):
pass
def repository_root(script_file: str) -> Path:
return Path(script_file).resolve().parents[2]
def load_json(path: Path):
try:
with path.open(encoding="utf-8") as source:
return json.load(source)
except (OSError, json.JSONDecodeError) as error:
raise ConfigError(f"cannot load {path}: {error}") from error
def object_value(value, label: str):
if not isinstance(value, dict):
raise ConfigError(f"{label} must be an object")
return value
def array_value(value, label: str):
if not isinstance(value, list):
raise ConfigError(f"{label} must be an array")
return value
def string_value(value, label: str):
if not isinstance(value, str) or not value:
raise ConfigError(f"{label} must be a non-empty string")
return value
def exact_keys(value: dict, expected: set[str], label: str):
actual = set(value)
if actual != expected:
missing = sorted(expected - actual)
extra = sorted(actual - expected)
raise ConfigError(f"{label} keys differ (missing={missing}, extra={extra})")
def schema_v1(value: dict, label: str):
schema = object_value(value.get("schema"), f"{label}.schema")
exact_keys(schema, {"version"}, f"{label}.schema")
if schema["version"] != 1:
raise ConfigError(f"{label}.schema.version must be 1")
def compact_json(value) -> str:
return json.dumps(value, separators=(",", ":"), sort_keys=True)
-108
View File
@@ -1,108 +0,0 @@
import { DIGEST_PATTERN, type Digest } from "./json.ts";
type Ensure = Readonly<{
array(value: unknown, label: string): unknown[];
digest(value: unknown, label: string): Digest;
exactKeys(value: Record<string, unknown>, allowed: readonly string[], label: string): void;
integer(value: unknown, label: string): number;
never(message: string): never;
record(value: unknown, label: string): Record<string, unknown>;
that(condition: unknown, message: string): asserts condition;
text(value: unknown, label: string): string;
}>;
export const ensure: Ensure = Object.freeze({
array(value, label) {
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
return value;
},
digest(value, label) {
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) throw new Error(`${label} must be a sha256 digest`);
return value as Digest;
},
exactKeys(value, allowed, label) {
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length > 0) throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
},
integer(value, label) {
if (!Number.isSafeInteger(value) || Number(value) < 1) throw new Error(`${label} must be a positive integer`);
return Number(value);
},
never(message) {
throw new Error(message);
},
record(value, label) {
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value as Record<string, unknown>;
},
that(condition, message) {
if (!condition) throw new Error(message);
},
text(value, label) {
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
return value;
},
});
export const fail = ensure.never;
export function switchBy<Key extends string, Result>(
key: Key,
branches: Readonly<Record<Key, () => Result>>,
): Result {
const branch = branches[key];
ensure.that(branch != null, `unsupported switch value: ${key}`);
return branch();
}
export class Options {
readonly #values: ReadonlyMap<string, string>;
constructor(values: Readonly<Record<string, string>>) {
this.#values = new Map(Object.entries(values));
}
get(name: string): string {
return this.#values.get(name) ?? ensure.never(`--${name} is required`);
}
optional(name: string): string | undefined {
return this.#values.get(name);
}
}
type Command = Readonly<{
options: readonly string[];
run: (args: Options) => Promise<void> | void;
}>;
export function command(options: readonly string[], run: Command["run"]): Command {
const sorted = [...options].sort();
ensure.that(new Set(sorted).size === sorted.length, "command options must not contain duplicates");
return Object.freeze({ options: Object.freeze(sorted), run });
}
export function commands<const Table extends Readonly<Record<string, Command>>>(
table: Table,
usage: () => string,
) {
return Object.freeze({
async run(argv: readonly string[]): Promise<void> {
const [name, ...args] = argv;
ensure.that(name != null && Object.hasOwn(table, name), `unknown command: ${name ?? ""}\n${usage()}`);
const selected = table[name as keyof Table]!;
ensure.that(args.length % 2 === 0, `${name} options must be --name value pairs`);
const values: Record<string, string> = {};
for (let index = 0; index < args.length; index += 2) {
const rawKey = args[index]!;
const key = rawKey.replace(/^--/u, "");
ensure.that(rawKey === `--${key}` && selected.options.includes(key), `${name} does not support ${rawKey}`);
ensure.that(values[key] == null, `${name} received duplicate option ${rawKey}`);
const value = args[index + 1];
ensure.that(value != null && !value.startsWith("--"), `${rawKey} requires a value`);
values[key] = value;
}
await selected.run(new Options(values));
},
});
}
-84
View File
@@ -1,84 +0,0 @@
import { execFileSync } from "node:child_process";
import { ensure, fail } from "./control.ts";
import { canonicalJson, digest, type Digest, type Json } from "./json.ts";
export type GitHashPath = Readonly<{
excludeDirectoryNames?: readonly string[];
excludePaths?: readonly string[];
normalizePackageVersion?: boolean;
normalizeTextLineEndings?: boolean;
path: string;
}>;
type GitEntry = Readonly<{ mode: string; object: string; path: string; type: string }>;
export function git(args: readonly string[], cwd: string): Buffer {
try {
return execFileSync("git", args, { cwd, encoding: "buffer", maxBuffer: 256 * 1024 * 1024 });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
fail(`git ${args.join(" ")} failed: ${detail}`);
}
}
function listGitEntries(root: string, ref: string, source: GitHashPath): readonly GitEntry[] {
const raw = git(["ls-tree", "-rz", "--full-tree", ref, "--", source.path], root);
const entries = raw.toString("utf8").split("\0").filter(Boolean).map((line): GitEntry => {
const tab = line.indexOf("\t");
if (tab < 0) fail(`invalid git ls-tree entry: ${line}`);
const [mode, type, object] = line.slice(0, tab).split(" ");
if (mode == null || type == null || object == null) fail(`invalid git ls-tree metadata: ${line}`);
return { mode, object, path: line.slice(tab + 1), type };
});
const excludedDirectories = new Set(source.excludeDirectoryNames ?? []);
const exclusions = (source.excludePaths ?? []).map((path) => `${source.path}/${path}`);
return entries.filter((entry) => {
const relativePath = entry.path === source.path ? "" : entry.path.slice(source.path.length + 1);
if (relativePath.split("/").some((segment) => excludedDirectories.has(segment))) return false;
return !exclusions.some((excluded) => entry.path === excluded || entry.path.startsWith(`${excluded}/`));
});
}
function normalizedBlob(root: string, entry: GitEntry, source: GitHashPath): Readonly<{ digest: Digest; size: number }> {
let body = git(["cat-file", "blob", entry.object], root);
if (source.normalizePackageVersion === true && (entry.path === "package.json" || entry.path.endsWith("/package.json"))) {
try {
const packageJson = ensure.record(JSON.parse(body.toString("utf8")), `${entry.path} package metadata`);
delete packageJson.version;
body = Buffer.from(`${canonicalJson(packageJson)}\n`);
} catch {
// The owning validation reports malformed JSON; identity remains byte-exact.
}
}
if (source.normalizeTextLineEndings === true && !body.includes(0)) {
body = Buffer.from(body.toString("utf8").replace(/\r\n?/gu, "\n"));
}
return { digest: digest(body), size: body.byteLength };
}
export function gitPathSetDigest(
root: string,
ref: string,
sources: readonly GitHashPath[],
options: Readonly<{ domain: string; label: string }>,
): Digest {
const seen = new Set<string>();
const entries: Json[] = [];
for (const source of sources) {
const matched = listGitEntries(root, ref, source);
if (matched.length === 0) fail(`${options.label} path does not exist at ${ref}: ${source.path}`);
for (const entry of matched) {
if (seen.has(entry.path)) fail(`${options.label} paths overlap at ${entry.path}`);
seen.add(entry.path);
if (entry.type === "blob") {
const normalized = normalizedBlob(root, entry, source);
entries.push({ kind: entry.mode === "120000" ? "symlink" : "file", mode: entry.mode, path: entry.path, ...normalized });
} else {
entries.push({ kind: entry.type, mode: entry.mode, object: entry.object, path: entry.path });
}
}
}
entries.sort((left, right) => String((left as Record<string, Json>).path).localeCompare(String((right as Record<string, Json>).path)));
return digest(canonicalJson({ domain: options.domain, entries }));
}
+19
View File
@@ -0,0 +1,19 @@
import os
from pathlib import Path
def append_outputs(values: dict[str, str]):
output_path = os.environ.get("GITHUB_OUTPUT")
lines = [f"{key}={value}" for key, value in values.items()]
if output_path:
with Path(output_path).open("a", encoding="utf-8") as output:
output.write("\n".join(lines) + "\n")
else:
print("\n".join(lines))
def append_summary(markdown: str):
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_path:
with Path(summary_path).open("a", encoding="utf-8") as summary:
summary.write(markdown.rstrip() + "\n")
-28
View File
@@ -1,28 +0,0 @@
import { createHash } from "node:crypto";
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
export type Digest = `sha256:${string}`;
export const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
export function canonicalize(value: unknown): Json {
if (value === null) return null;
if (typeof value === "boolean" || typeof value === "string") return value;
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new Error("canonical JSON cannot contain non-finite numbers");
return value;
}
if (Array.isArray(value)) return value.map(canonicalize);
if (value == null || typeof value !== "object") throw new Error("canonical JSON value must be an object");
return Object.fromEntries(Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, canonicalize(entry)]));
}
export function canonicalJson(value: unknown): string {
return JSON.stringify(canonicalize(value));
}
export function digest(value: string | Uint8Array): Digest {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
+1 -1
View File
@@ -127,7 +127,7 @@ def annotation_indicates_ordinary_failure(item: dict[str, Any]) -> bool:
Notice- and warning-level annotations without markers are setup / policy
noise and must not reclassify a pure runner-shutdown job as ordinary.
Concrete repository path: ``scripts/scopes.ts`` emits ``::warning::`` when
Concrete repository path: ``.github/scripts/scopes.py`` emits ``::warning::`` when
changed-file resolution falls back to the full plan; that warning must not
suppress infra-cancel retry when a later shutdown marker is also present.
+91 -73
View File
@@ -1,90 +1,108 @@
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
from typing import Optional
from lib.config import (
ConfigError,
array_value,
compact_json,
exact_keys,
load_json,
object_value,
repository_root,
schema_v1,
string_value,
)
from lib.github import append_outputs
GITHUB_HOSTED = ["ubuntu-24.04"]
WINDOWS_HOSTED = ["windows-latest"]
BLACKSMITH_4VCPU = ["blacksmith-4vcpu-ubuntu-2404"]
BLACKSMITH_8VCPU = ["blacksmith-8vcpu-ubuntu-2404"]
NEXU_SMALL = ["nexu-runners-small"]
NEXU_MEDIUM = ["nexu-runners-medium"]
NEXU_LARGE = ["nexu-runners-large"]
NEXU_XLARGE = ["nexu-runners-xlarge"]
EXPECTED_CLASSES = {
"control",
"general_medium",
"workspace_unit",
"windows_tools",
"js_hot",
"ui_hot",
"ui_p0",
"ui_p0_heavy",
"visual_hot",
}
def compact_json(value):
return json.dumps(value, separators=(",", ":"))
def load_contract(path: Path):
config = object_value(load_json(path), "runners")
exact_keys(
config,
{"schema", "defaultMode", "aliases", "profiles", "modes"},
"runners",
)
schema_v1(config, "runners")
default_mode = string_value(config["defaultMode"], "runners.defaultMode")
aliases = object_value(config["aliases"], "runners.aliases")
profiles = object_value(config["profiles"], "runners.profiles")
modes = object_value(config["modes"], "runners.modes")
normalized_profiles = {}
for name, labels in profiles.items():
string_value(name, "runners.profiles key")
labels = array_value(labels, f"runners.profiles.{name}")
if not labels:
raise ConfigError(f"runners.profiles.{name} must not be empty")
normalized_profiles[name] = [
string_value(label, f"runners.profiles.{name}[]") for label in labels
]
normalized_modes = {}
for name, assignments in modes.items():
string_value(name, "runners.modes key")
assignments = object_value(assignments, f"runners.modes.{name}")
exact_keys(assignments, EXPECTED_CLASSES, f"runners.modes.{name}")
normalized_modes[name] = {}
for runner_class, profile in assignments.items():
profile = string_value(profile, f"runners.modes.{name}.{runner_class}")
if profile not in normalized_profiles:
raise ConfigError(
f"runners.modes.{name}.{runner_class} references unknown profile {profile}"
)
normalized_modes[name][runner_class] = normalized_profiles[profile]
if default_mode not in normalized_modes:
raise ConfigError(f"runners.defaultMode references unknown mode {default_mode}")
for alias, target in aliases.items():
string_value(alias, "runners.aliases key")
target = string_value(target, f"runners.aliases.{alias}")
if target not in normalized_modes:
raise ConfigError(f"runners.aliases.{alias} references unknown mode {target}")
return default_mode, aliases, normalized_modes
def normalize_mode(raw_mode):
mode = raw_mode or "default"
if mode in {"default", "performance", "economic", "blacksmith"}:
return mode
return "default"
def resolve_contract(mode):
if mode == "economic":
control = GITHUB_HOSTED
workload = GITHUB_HOSTED
browser_workload = GITHUB_HOSTED
ui_p0_workload = GITHUB_HOSTED
ui_p0_heavy_workload = GITHUB_HOSTED
elif mode == "blacksmith":
control = BLACKSMITH_4VCPU
workload = BLACKSMITH_4VCPU
browser_workload = BLACKSMITH_8VCPU
# Blacksmith has no xlarge tier; keep both UI P0 classes on 8vcpu.
ui_p0_workload = BLACKSMITH_8VCPU
ui_p0_heavy_workload = BLACKSMITH_8VCPU
else:
control = NEXU_SMALL
workload = NEXU_MEDIUM
browser_workload = NEXU_LARGE
# Measured UI P0 shards fit medium (mem well under 14Gi lim). Keep a
# dedicated heavy class for multi-client collab, which has hit large/xlarge
# memory ceilings and is not covered by the medium right-size evidence.
ui_p0_workload = NEXU_MEDIUM
ui_p0_heavy_workload = NEXU_XLARGE
def resolve(path: Path, requested_mode: Optional[str]):
default_mode, aliases, modes = load_contract(path)
selected = requested_mode or default_mode
selected = aliases.get(selected, selected)
if selected not in modes:
raise ConfigError(f"unknown runner mode: {selected}")
return {
"runs_on": {
"control": control,
"general_medium": workload,
"workspace_unit": workload,
"windows_tools": WINDOWS_HOSTED,
"js_hot": workload,
"ui_hot": browser_workload,
"ui_p0": ui_p0_workload,
"ui_p0_heavy": ui_p0_heavy_workload,
"visual_hot": browser_workload,
},
"decision": {
"schema_version": 1,
"mode": mode,
},
"runs_on": modes[selected],
"decision": {"schema_version": 1, "mode": selected},
}
def main():
contract = resolve_contract(normalize_mode(os.environ.get("OD_CI_RUNNER_MODE")))
output_path = os.environ.get("GITHUB_OUTPUT")
lines = [
f"{key}={value if isinstance(value, str) else compact_json(value)}"
for key, value in contract.items()
]
if output_path:
with Path(output_path).open("a", encoding="utf-8") as output:
for line in lines:
output.write(f"{line}\n")
else:
for line in lines:
print(line)
def main() -> int:
root = repository_root(__file__)
config_path = root / ".github/config/runners.json"
try:
contract = resolve(config_path, os.environ.get("OD_CI_RUNNER_MODE"))
except ConfigError as error:
print(f"runner configuration error: {error}", file=sys.stderr)
return 2
append_outputs({key: compact_json(value) for key, value in contract.items()})
return 0
if __name__ == "__main__":
main()
raise SystemExit(main())
+373
View File
@@ -0,0 +1,373 @@
#!/usr/bin/env python3
import argparse
import json
import os
import re
import subprocess
import sys
from collections import Counter
from pathlib import Path
from lib.config import ConfigError, compact_json, load_json, object_value, repository_root, schema_v1
from lib.github import append_outputs, append_summary
CONFIDENCE = {"medium": 0, "certain": 1}
WORKLOADS = {
"static_gate",
"preflight",
"workspace_unit_tests",
"daemon_unit_tests",
"windows_tools_pack_payload_tests",
"web_workspace_tests",
"e2e_vitest",
"playwright_critical",
"ui_p0",
"playwright_visual",
}
class ScopeContract:
def __init__(self, path: Path):
value = object_value(load_json(path), "scopes")
expected = {"schema", "effects", "matches", "rules", "matrices", "uiP0Shadow"}
if set(value) != expected:
raise ConfigError(f"scopes keys must be {sorted(expected)}")
schema_v1(value, "scopes")
self.effects = self._string_list(value["effects"], "scopes.effects")
if len(set(self.effects)) != len(self.effects):
raise ConfigError("scopes.effects contains duplicates")
self.matches = object_value(value["matches"], "scopes.matches")
self.rules = value["rules"]
self.matrices = object_value(value["matrices"], "scopes.matrices")
self.shadow = object_value(value["uiP0Shadow"], "scopes.uiP0Shadow")
self._validate()
@staticmethod
def _string_list(value, label):
if not isinstance(value, list) or any(not isinstance(item, str) or not item for item in value):
raise ConfigError(f"{label} must be an array of non-empty strings")
return value
def _validate_match(self, match, label, stack=()):
match = object_value(match, label)
allowed = {"prefixes", "exact", "regexes", "include", "exclude"}
if not set(match) <= allowed:
raise ConfigError(f"{label} has unsupported keys {sorted(set(match) - allowed)}")
for field in ("prefixes", "exact", "regexes", "include", "exclude"):
if field in match:
values = self._string_list(match[field], f"{label}.{field}")
for token in values:
if field in {"include", "exclude"}:
self._validate_token(token, f"{label}.{field}", stack)
elif field == "regexes":
try:
re.compile(token)
except re.error as error:
raise ConfigError(f"invalid regex {token!r} in {label}: {error}") from error
def _validate_token(self, token, label, stack):
if token.startswith("match://"):
name = token.removeprefix("match://")
if name not in self.matches:
raise ConfigError(f"{label} references unknown match {name}")
if name in stack:
raise ConfigError(f"match cycle: {' -> '.join((*stack, name))}")
self._validate_match(self.matches[name], f"scopes.matches.{name}", (*stack, name))
elif not token.startswith("prefix://"):
raise ConfigError(f"{label} has unsupported token {token}")
def _validate(self):
for name, match in self.matches.items():
if not isinstance(name, str) or not name:
raise ConfigError("scopes.matches keys must be non-empty strings")
self._validate_match(match, f"scopes.matches.{name}", (name,))
if not isinstance(self.rules, list) or not self.rules:
raise ConfigError("scopes.rules must be a non-empty array")
seen = set()
for index, rule in enumerate(self.rules):
rule = object_value(rule, f"scopes.rules[{index}]")
allowed = {"id", "match", "effects", "confidence"}
if not set(rule) <= allowed or not {"id", "match", "effects", "confidence"} <= set(rule):
raise ConfigError(f"scopes.rules[{index}] has invalid keys")
rule_id = rule["id"]
if not isinstance(rule_id, str) or not rule_id or rule_id in seen:
raise ConfigError(f"invalid or duplicate scope rule id {rule_id!r}")
seen.add(rule_id)
self._validate_match(rule["match"], f"scopes.rules.{rule_id}.match")
effects = self._string_list(rule["effects"], f"scopes.rules.{rule_id}.effects")
unknown = set(effects) - set(self.effects)
if unknown:
raise ConfigError(f"scope rule {rule_id} has unknown effects {sorted(unknown)}")
if rule["confidence"] not in CONFIDENCE:
raise ConfigError(f"scope rule {rule_id} has invalid confidence")
if set(self.matrices) != {"ui_p0", "visual"}:
raise ConfigError("scopes.matrices must contain ui_p0 and visual")
matrix_names = {}
for matrix_name, fields in (("ui_p0", {"name", "shard"}), ("visual", {"name", "files"})):
entries = self.matrices[matrix_name]
if not isinstance(entries, list) or not entries:
raise ConfigError(f"scopes.matrices.{matrix_name} must be a non-empty array")
names = []
for index, entry in enumerate(entries):
entry = object_value(entry, f"scopes.matrices.{matrix_name}[{index}]")
if set(entry) != fields:
raise ConfigError(f"scopes.matrices.{matrix_name}[{index}] keys must be {sorted(fields)}")
for field in fields:
if not isinstance(entry[field], str) or not entry[field]:
raise ConfigError(f"scopes.matrices.{matrix_name}[{index}].{field} must be a non-empty string")
names.append(entry["name"])
if len(set(names)) != len(names):
raise ConfigError(f"scopes.matrices.{matrix_name} contains duplicate names")
matrix_names[matrix_name] = set(names)
if set(self.shadow) != {"match", "matrixNames"}:
raise ConfigError("scopes.uiP0Shadow keys must be match and matrixNames")
shadow_match = self.shadow.get("match")
if shadow_match not in self.matches:
raise ConfigError("scopes.uiP0Shadow.match is unknown")
shadow_names = self._string_list(self.shadow.get("matrixNames"), "scopes.uiP0Shadow.matrixNames")
if len(set(shadow_names)) != len(shadow_names):
raise ConfigError("scopes.uiP0Shadow.matrixNames contains duplicates")
unknown_shadow_names = set(shadow_names) - matrix_names["ui_p0"]
if unknown_shadow_names:
raise ConfigError(f"scopes.uiP0Shadow.matrixNames contains unknown names {sorted(unknown_shadow_names)}")
def _token_matches(self, file, token):
if token.startswith("match://"):
return self.match(file, self.matches[token.removeprefix("match://")])
return file.startswith(token.removeprefix("prefix://"))
def match(self, file, match):
positives = []
positives.extend(file.startswith(prefix) for prefix in match.get("prefixes", []))
positives.extend(file == exact for exact in match.get("exact", []))
positives.extend(re.search(pattern, file) is not None for pattern in match.get("regexes", []))
positives.extend(self._token_matches(file, token) for token in match.get("include", []))
if positives and not any(positives):
return False
if any(self._token_matches(file, token) for token in match.get("exclude", [])):
return False
return True
def empty_effects(contract):
return {effect: False for effect in contract.effects}
def evaluate(contract, files, threshold, derive_workspace):
outputs = empty_effects(contract)
decisions = []
for file in files:
matched = [rule for rule in contract.rules if contract.match(file, rule["match"])]
matched_ids = [rule["id"] for rule in matched]
if not matched:
outputs = {effect: True for effect in contract.effects}
decisions.append({"file": file, "matchedRules": [], "escalated": True, "reason": "unmatched"})
continue
if any(CONFIDENCE[rule["confidence"]] < CONFIDENCE[threshold] for rule in matched):
outputs = {effect: True for effect in contract.effects}
decisions.append({"file": file, "matchedRules": matched_ids, "escalated": True, "reason": "below-threshold"})
continue
for rule in matched:
for effect in rule["effects"]:
outputs[effect] = True
decisions.append({"file": file, "matchedRules": matched_ids, "escalated": False})
if derive_workspace and any(outputs[name] for name in (
"daemon_tests_required", "web_tests_required", "tools_dev_tests_required", "tools_pack_tests_required"
)):
outputs["workspace_validation_required"] = True
return outputs, decisions
def enabled_workloads(outputs, ci_mode, full_lanes):
any_scope = any(outputs.values())
broad = full_lanes or ci_mode == "hot" or any_scope
ui_p0 = full_lanes or outputs["ui_p0_validation_required"]
enabled = {
"static_gate": True,
"preflight": True,
"workspace_unit_tests": broad,
"daemon_unit_tests": outputs["daemon_tests_required"],
"windows_tools_pack_payload_tests": full_lanes or outputs["tools_pack_tests_required"],
"web_workspace_tests": full_lanes or outputs["web_tests_required"],
"e2e_vitest": full_lanes or outputs["web_tests_required"] or outputs["ui_p0_validation_required"],
"playwright_critical": outputs["ui_critical_validation_required"] and not ui_p0,
"ui_p0": ui_p0,
"playwright_visual": full_lanes or outputs["visual_validation_required"],
}
if set(enabled) != WORKLOADS:
raise AssertionError("scope workload map drifted")
return enabled, broad
def build_plan(contract, files, source, threshold, ci_mode, full_lanes, derive_workspace, resolved=True):
if threshold is None:
outputs = {effect: True for effect in contract.effects}
decisions = []
else:
outputs, decisions = evaluate(contract, files, threshold, derive_workspace)
enabled, broad = enabled_workloads(outputs, ci_mode, full_lanes)
hits = Counter(rule for decision in decisions for rule in decision["matchedRules"])
shadow_match = contract.matches[contract.shadow["match"]]
candidate = bool(files) and resolved and all(contract.match(file, shadow_match) for file in files)
shadow_names = set(contract.shadow["matrixNames"])
shadow_matrix = [entry for entry in contract.matrices["ui_p0"] if entry["name"] in shadow_names] if candidate else contract.matrices["ui_p0"]
scopes = {
**outputs,
"ci_mode": ci_mode,
"run_preflight_typecheck": broad,
}
return {
"schemaVersion": 1,
"source": source,
"scopes": scopes,
"enabled": enabled,
"matrices": contract.matrices,
"trace": {
"threshold": threshold or "none",
"filesResolved": resolved,
"fileCount": len(files) if resolved else 0,
"ruleHits": dict(hits),
"escalations": [
{"file": item["file"], "reason": item["reason"]}
for item in decisions if item["escalated"]
],
"uiP0Shadow": {
"mode": "candidate" if candidate else "full-fallback",
"matrix": shadow_matrix,
},
},
}
def required_env(name):
value = os.environ.get(name)
if not value:
raise ConfigError(f"{name} is required")
return value
def event_payload():
return load_json(Path(required_env("GITHUB_EVENT_PATH")))
def run_gh(args):
override = os.environ.get("OPEN_DESIGN_GH_NODE_SCRIPT")
command = (["node", override] if override else ["gh"]) + args
return subprocess.run(command, check=True, text=True, stdout=subprocess.PIPE).stdout
def changed_files_for_environment():
event_name = required_env("GITHUB_EVENT_NAME")
repository = required_env("GITHUB_REPOSITORY")
event = event_payload()
if event_name == "pull_request":
number = event.get("pull_request", {}).get("number")
if number is None:
raise ConfigError("pull_request.number is required")
output = run_gh(["api", "--paginate", f"repos/{repository}/pulls/{number}/files", "--jq", ".[] | .filename, (.previous_filename // empty)"])
return event_name, output.splitlines(), "medium", "hot", False, True, True
if event_name == "workflow_dispatch":
mode = event.get("inputs", {}).get("ci_mode", "full")
if mode not in {"hot", "full"}:
raise ConfigError(f"unsupported workflow_dispatch ci_mode: {mode}")
if mode == "hot":
sha = required_env("GITHUB_SHA")
output = run_gh(["api", "--paginate", f"repos/{repository}/compare/main...{sha}", "--jq", "(.files // [])[] | .filename, (.previous_filename // empty)"])
return "workflow_dispatch:hot", output.splitlines(), "medium", "hot", False, False, True
return event_name, [], None, "full", True, False, False
if event_name == "merge_group":
group = event.get("merge_group", {})
base, head = group.get("base_sha"), group.get("head_sha")
if not base or not head:
raise ConfigError("merge_group base_sha and head_sha are required")
try:
comparison = json.loads(run_gh(["api", f"repos/{repository}/compare/{base}...{head}"]))
records = comparison.get("files", [])
if len(records) >= 300:
raise ConfigError("merge_group comparison reached the 300-file ceiling")
files = [name for record in records for name in (record.get("filename"), record.get("previous_filename")) if isinstance(name, str)]
if not files:
return "merge_group:empty-resolution", [], None, "full", True, True, False
return event_name, files, "certain", "full", False, True, True
except (subprocess.SubprocessError, json.JSONDecodeError, ConfigError) as error:
print(f"::warning::merge_group resolution failed; using full plan: {error}", file=sys.stderr)
return "merge_group:resolution-error", [], None, "full", True, True, False
return event_name, [], None, "full", True, False, False
def emit_plan(plan, output_path=None):
if output_path:
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(plan, indent=2, sort_keys=True) + "\n", encoding="utf-8")
append_outputs({
"scopes": compact_json(plan["scopes"]),
"ui_p0_matrix": compact_json(plan["matrices"]["ui_p0"]),
"visual_matrix": compact_json(plan["matrices"]["visual"]),
})
trace = plan["trace"]
lines = [
"### Scope decision trace", "",
f"- source: `{plan['source']}`, trust threshold: `{trace['threshold']}`",
f"- files: {trace['fileCount'] if trace['filesResolved'] else 'not resolved'}, escalated: {len(trace['escalations'])}",
f"- UI P0 shadow: `{trace['uiP0Shadow']['mode']}`",
]
if trace["ruleHits"]:
lines += ["", "| Rule | Hits |", "| --- | ---: |"]
lines += [f"| {name} | {count} |" for name, count in sorted(trace["ruleHits"].items(), key=lambda item: -item[1])]
append_summary("\n".join(lines))
print("scope decision trace:\n" + json.dumps(trace, indent=2, sort_keys=True))
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path)
sub = parser.add_subparsers(dest="command", required=True)
github = sub.add_parser("github-output")
github.add_argument("--output", type=Path)
plan = sub.add_parser("plan")
plan.add_argument("--context", choices=("pr", "merge-queue", "full"), default="pr")
plan.add_argument("--files", nargs="*", default=[])
plan.add_argument("--files-from")
sub.add_parser("validate")
sub.add_parser("rules")
return parser.parse_args()
def main():
root = repository_root(__file__)
args = parse_args()
contract = ScopeContract(args.config or root / ".github/config/scopes.json")
if args.command == "validate":
print("scope configuration is valid")
return 0
if args.command == "rules":
for rule in contract.rules:
print(f"{rule['id']}\n confidence: {rule['confidence']}\n effects: {', '.join(rule['effects']) or '(none)'}")
return 0
if args.command == "github-output":
source, files, threshold, mode, full_lanes, derive, resolved = changed_files_for_environment()
emit_plan(build_plan(contract, files, source, threshold, mode, full_lanes, derive, resolved), args.output)
return 0
files = list(args.files)
if args.files_from:
content = sys.stdin.read() if args.files_from == "-" else Path(args.files_from).read_text(encoding="utf-8")
files += [line for line in content.splitlines() if line]
if args.context == "full":
settings = (None, "full", True, False, False)
elif args.context == "merge-queue":
settings = ("certain", "full", False, True, True)
else:
settings = ("medium", "hot", False, True, True)
threshold, mode, full_lanes, derive, resolved = settings
print(json.dumps(build_plan(contract, files, f"cli:{args.context}", threshold, mode, full_lanes, derive, resolved), indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ConfigError as error:
print(f"scope configuration error: {error}", file=sys.stderr)
raise SystemExit(2)
+89 -69
View File
@@ -59,49 +59,60 @@ jobs:
|| vars.OD_CI_RUNNER_MODE }}
run: python3 .github/scripts/runners.py
scopes:
name: Detect validation scopes
plan:
name: Plan validation workloads
needs: [runners]
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).control }}
outputs:
ci_mode: ${{ steps.detect.outputs.ci_mode }}
daemon_tests_required: ${{ steps.detect.outputs.daemon_tests_required }}
web_tests_required: ${{ steps.detect.outputs.web_tests_required }}
tools_dev_tests_required: ${{ steps.detect.outputs.tools_dev_tests_required }}
tools_pack_tests_required: ${{ steps.detect.outputs.tools_pack_tests_required }}
ui_p0_validation_required: ${{ steps.detect.outputs.ui_p0_validation_required }}
visual_validation_required: ${{ steps.detect.outputs.visual_validation_required }}
workspace_validation_required: ${{ steps.detect.outputs.workspace_validation_required }}
run_e2e_vitest: ${{ steps.detect.outputs.run_e2e_vitest }}
run_playwright_critical: ${{ steps.detect.outputs.run_playwright_critical }}
run_playwright_visual: ${{ steps.detect.outputs.run_playwright_visual }}
run_preflight: ${{ steps.detect.outputs.run_preflight }}
run_preflight_typecheck: ${{ steps.detect.outputs.run_preflight_typecheck }}
run_ui_p0: ${{ steps.detect.outputs.run_ui_p0 }}
run_web_workspace_tests: ${{ steps.detect.outputs.run_web_workspace_tests }}
run_windows_tools_pack_payload_tests: ${{ steps.detect.outputs.run_windows_tools_pack_payload_tests }}
run_workspace_unit_tests: ${{ steps.detect.outputs.run_workspace_unit_tests }}
ui_p0_matrix: ${{ steps.detect.outputs.ui_p0_matrix }}
visual_matrix: ${{ steps.detect.outputs.visual_matrix }}
run: ${{ steps.hash.outputs.run }}
equal: ${{ steps.hash.outputs.equal }}
scopes: ${{ steps.scopes.outputs.scopes }}
ui_p0_matrix: ${{ steps.scopes.outputs.ui_p0_matrix }}
visual_matrix: ${{ steps.scopes.outputs.visual_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Detect workspace and app test scopes
id: detect
- name: Resolve validation scopes
id: scopes
env:
GH_TOKEN: ${{ github.token }}
run: node --experimental-strip-types scripts/scopes.ts github-output
run: python3 .github/scripts/scopes.py github-output --output "$RUNNER_TEMP/scope-plan.json"
- name: Restore previous hash map
id: hash_cache
continue-on-error: true
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/hash-state.json
key: ci-hash-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
ci-hash-${{ runner.os }}-${{ github.ref }}-
- name: Compare and replace hash map
id: hash
run: >-
python3 .github/scripts/hash.py github-output
--workflow ci
--scope-plan "$RUNNER_TEMP/scope-plan.json"
--state "$RUNNER_TEMP/hash-state.json"
# Carry the freshly computed comparison register to the gate without
# publishing it as reusable cache state. A failed gate must leave the
# previously successful map in place so an identical retry runs again.
- name: Upload pending hash map
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: ci-hash-state-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/hash-state.json
if-no-files-found: error
retention-days: 1
- name: Create visual report handoff
id: visual_report_handoff
if: ${{ github.event_name == 'pull_request' && steps.detect.outputs.run_playwright_visual == 'true' }}
if: ${{ github.event_name == 'pull_request' && fromJSON(steps.hash.outputs.run).playwright_visual }}
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -145,7 +156,8 @@ jobs:
static_gate:
name: Static gate
needs: [runners]
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).static_gate }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).control }}
timeout-minutes: 10
env:
@@ -236,8 +248,8 @@ jobs:
preflight:
name: Preflight
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_preflight == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).preflight }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).general_medium }}
timeout-minutes: 45
@@ -269,14 +281,14 @@ jobs:
# If postinstall grows a targeted app type-generation phase covering these
# three exports without broad app builds, this CI prebuild can be removed.
- name: Prebuild workspace type declarations
if: ${{ needs.scopes.outputs.run_preflight_typecheck == 'true' }}
if: ${{ fromJSON(needs.plan.outputs.scopes).run_preflight_typecheck }}
run: |
pnpm --filter @open-design/daemon build
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/web build:sidecar
- name: Typecheck workspaces
if: ${{ needs.scopes.outputs.run_preflight_typecheck == 'true' }}
if: ${{ fromJSON(needs.plan.outputs.scopes).run_preflight_typecheck }}
run: |
pnpm -r --filter '!open-design' --filter '!@open-design/landing-page' --workspace-concurrency="${OPEN_DESIGN_WORKSPACE_CONCURRENCY:-1}" --if-present run typecheck
pnpm exec tsc -p scripts/tsconfig.json --noEmit
@@ -289,8 +301,8 @@ jobs:
workspace_unit_tests:
name: Workspace unit tests
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_workspace_unit_tests == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).workspace_unit_tests }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).workspace_unit }}
timeout-minutes: 20
@@ -313,15 +325,15 @@ jobs:
pnpm --filter @open-design/platform test
pnpm --filter @open-design/sidecar test
pnpm --filter @open-design/sidecar-proto test
if [ "${{ needs.scopes.outputs.tools_dev_tests_required }}" = "true" ]; then
if [ "${{ fromJSON(needs.plan.outputs.scopes).tools_dev_tests_required }}" = "true" ]; then
pnpm --filter @open-design/tools-dev test
fi
if [ "${{ needs.scopes.outputs.tools_pack_tests_required }}" = "true" ]; then
if [ "${{ fromJSON(needs.plan.outputs.scopes).tools_pack_tests_required }}" = "true" ]; then
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/desktop test
pnpm --filter @open-design/packaged test
pnpm --filter @open-design/tools-pack test
if [ "${{ needs.scopes.outputs.run_e2e_vitest }}" != "true" ]; then
if [ "${{ fromJSON(needs.plan.outputs.run).e2e_vitest }}" != "true" ]; then
pnpm --filter @open-design/e2e test tests/packaged-launcher-update-loop.test.ts
fi
fi
@@ -329,8 +341,8 @@ jobs:
daemon_unit_tests:
name: Daemon tests (${{ matrix.shard }}/4)
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.daemon_tests_required == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).daemon_unit_tests }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).workspace_unit }}
timeout-minutes: 15
strategy:
@@ -357,8 +369,8 @@ jobs:
windows_tools_pack_payload_tests:
name: Windows tools-pack payload tests
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_windows_tools_pack_payload_tests == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).windows_tools_pack_payload_tests }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).windows_tools }}
timeout-minutes: 20
@@ -376,8 +388,8 @@ jobs:
web_workspace_tests:
name: Web workspace tests (${{ matrix.shard }}/2)
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_web_workspace_tests == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).web_workspace_tests }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).js_hot }}
timeout-minutes: 20
strategy:
@@ -405,8 +417,8 @@ jobs:
e2e_vitest:
name: E2E Vitest
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_e2e_vitest == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).e2e_vitest }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).js_hot }}
timeout-minutes: 20
@@ -440,8 +452,8 @@ jobs:
playwright_critical:
name: Playwright critical (${{ matrix.group }})
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_playwright_critical == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).playwright_critical }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).ui_hot }}
timeout-minutes: 30
strategy:
@@ -488,14 +500,14 @@ jobs:
ui_p0:
name: UI P0 (${{ matrix.name }})
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_ui_p0 == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).ui_p0 }}
runs-on: ${{ matrix.shard == 'project-collab' && fromJSON(needs.runners.outputs.runs_on).ui_p0_heavy || fromJSON(needs.runners.outputs.runs_on).ui_p0 }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}
include: ${{ fromJSON(needs.plan.outputs.ui_p0_matrix) }}
steps:
- name: Checkout
@@ -560,14 +572,14 @@ jobs:
playwright_visual:
name: Playwright visual (${{ matrix.name }})
needs: [scopes, runners]
if: ${{ needs.scopes.outputs.run_playwright_visual == 'true' }}
needs: [plan, runners]
if: ${{ fromJSON(needs.plan.outputs.run).playwright_visual }}
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).visual_hot }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.scopes.outputs.visual_matrix) }}
include: ${{ fromJSON(needs.plan.outputs.visual_matrix) }}
steps:
- name: Checkout
@@ -652,7 +664,7 @@ jobs:
validate:
name: Validate workspace
needs:
- scopes
- plan
- runners
- static_gate
- preflight
@@ -698,19 +710,10 @@ jobs:
required_misses="$(echo "$NEEDS_JSON" | jq -r '
. as $needs |
($needs.scopes.outputs // {}) as $out |
def when($condition; $jobs): if $condition then $jobs else [] end;
(($needs.plan.outputs.run // "{}") | fromjson) as $run |
(
["scopes", "static_gate"]
+ when($out.run_preflight == "true"; ["preflight"])
+ when($out.run_workspace_unit_tests == "true"; ["workspace_unit_tests"])
+ when($out.daemon_tests_required == "true"; ["daemon_unit_tests"])
+ when($out.run_windows_tools_pack_payload_tests == "true"; ["windows_tools_pack_payload_tests"])
+ when($out.run_web_workspace_tests == "true"; ["web_workspace_tests"])
+ when($out.run_e2e_vitest == "true"; ["e2e_vitest"])
+ when($out.run_playwright_critical == "true"; ["playwright_critical"])
+ when($out.run_ui_p0 == "true"; ["ui_p0"])
+ when($out.run_playwright_visual == "true"; ["playwright_visual"])
["plan"]
+ [$run | to_entries[] | select(.value) | .key]
)[]
| select(($needs[.].result // "missing") != "success")
| "\(.)=\($needs[.].result // "missing")"
@@ -892,6 +895,23 @@ jobs:
name: ${{ steps.merge_blocking_label_gate.outputs.comment_name }}
path: ${{ steps.merge_blocking_label_gate.outputs.comment_path }}
- name: Download pending hash map
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: ci-hash-state-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}
# Publish only after every merge-gating step has passed. The cache still
# contains only the identity-to-hash comparison register; successful gate
# completion controls publication rather than becoming cache payload.
- name: Save successful hash map
uses: actions/cache/save@v5
continue-on-error: true
with:
path: ${{ runner.temp }}/hash-state.json
key: ci-hash-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}
runtime_summary:
name: Runtime summary
needs:
+94 -70
View File
@@ -88,7 +88,7 @@ const releaseStableNotesScriptPath = join(workspaceRoot, ".github", "scripts", "
const releaseStableScriptPath = join(workspaceRoot, "tools", "release", "src", "metadata", "prepare-stable.ts");
const releaseBetaScriptPath = join(workspaceRoot, "tools", "release", "src", "metadata", "prepare-beta.ts");
const packagedPackageJsonPath = join(workspaceRoot, "apps", "packaged", "package.json");
const scopesScriptPath = join(workspaceRoot, "scripts", "scopes.ts");
const scopesScriptPath = join(workspaceRoot, ".github", "scripts", "scopes.py");
const runnersScriptPath = join(workspaceRoot, ".github", "scripts", "runners.py");
const notifyDailyFeishuWorkflowPath = join(workspaceRoot, ".github", "workflows", "notify-daily-feishu.yml");
const notifyReleaseFeishuWorkflowPath = join(workspaceRoot, ".github", "workflows", "notify-release-feishu.yml");
@@ -253,39 +253,33 @@ async function readPackagedVersion(): Promise<string> {
}
async function runScopesPrint(eventName: string, eventPayload: unknown, changedFiles: string[] = []): Promise<Record<string, unknown>> {
const tempDir = await mkdtemp(join(tmpdir(), "od-scopes-"));
const eventPath = join(tempDir, "event.json");
const ghPath = join(tempDir, "gh");
const ghCmdPath = join(tempDir, "gh.cmd");
await writeFile(eventPath, JSON.stringify(eventPayload));
const script = `#!/usr/bin/env node
const changedFiles = ${JSON.stringify(changedFiles)};
if (process.argv.includes("--jq")) {
process.stdout.write(changedFiles.join("\\n"));
if (changedFiles.length > 0) process.stdout.write("\\n");
} else {
process.stdout.write(JSON.stringify({ files: changedFiles.map((filename) => ({ filename })) }));
}
`;
await writeFile(ghPath, script);
await chmod(ghPath, 0o755);
await writeFile(ghCmdPath, `@echo off\r\n"${process.execPath}" "${ghPath}" %*\r\n`);
try {
const { stdout } = await execFileAsync(process.execPath, ["--experimental-strip-types", scopesScriptPath, "print"], {
cwd: workspaceRoot,
env: workflowFixtureEnv({
GITHUB_EVENT_NAME: eventName,
GITHUB_EVENT_PATH: eventPath,
GITHUB_REPOSITORY: "nexu-io/open-design",
GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567",
OPEN_DESIGN_GH_NODE_SCRIPT: ghPath,
}, tempDir),
});
return JSON.parse(stdout) as Record<string, unknown>;
} finally {
await rm(tempDir, { recursive: true, force: true });
}
const inputMode = (eventPayload as { inputs?: { ci_mode?: string } }).inputs?.ci_mode;
const context =
eventName === "workflow_dispatch" && inputMode !== "hot"
? "full"
: eventName === "merge_group" && changedFiles.length === 0
? "full"
: eventName === "merge_group"
? "merge-queue"
: "pr";
const { stdout } = await execFileAsync("python3", [scopesScriptPath, "plan", "--context", context, "--files", ...changedFiles], {
cwd: workspaceRoot,
});
const value = JSON.parse(stdout) as {
scopes: Record<string, unknown>;
enabled: Record<string, boolean>;
};
return {
...value.scopes,
run_e2e_vitest: value.enabled.e2e_vitest,
run_playwright_critical: value.enabled.playwright_critical,
run_playwright_visual: value.enabled.playwright_visual,
run_preflight: value.enabled.preflight,
run_ui_p0: value.enabled.ui_p0,
run_web_workspace_tests: value.enabled.web_workspace_tests,
run_windows_tools_pack_payload_tests: value.enabled.windows_tools_pack_payload_tests,
run_workspace_unit_tests: value.enabled.workspace_unit_tests,
};
}
async function runRunners(mode?: string): Promise<Record<string, string>> {
@@ -407,7 +401,6 @@ describe("packaged smoke workflow", () => {
expect(workflow).not.toContain("Smoke PR windows packaged runtime");
expect(workflow).not.toContain("Smoke PR linux headless packaged runtime");
expect(workflow).not.toContain("OD_PACKAGED_E2E_");
expect(workflow).not.toContain("actions/cache/save");
});
it("[P2] runs Windows launcher payload archive validation when tools-pack is touched", async () => {
@@ -417,7 +410,7 @@ describe("packaged smoke workflow", () => {
expect(job).toContain("fromJSON(needs.runners.outputs.runs_on).windows_tools");
expect(job).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).windows_tools)");
expect(job).toContain("needs.scopes.outputs.run_windows_tools_pack_payload_tests == 'true'");
expect(job).toContain("fromJSON(needs.plan.outputs.run).windows_tools_pack_payload_tests");
expect(job).toContain("pnpm --filter @open-design/tools-pack exec vitest run tests/launcher-payload.test.ts");
expect(validate).toContain("windows_tools_pack_payload_tests");
});
@@ -1075,15 +1068,14 @@ process.stdin.on("end", () => {
it("[P2] keeps PR and merge queue CI separated by hot/full validation mode", async () => {
const workflow = await readFile(ciWorkflowPath, "utf8");
const scopes = sectionBetween(workflow, " scopes:", " static_gate:");
const plan = sectionBetween(workflow, " plan:", " static_gate:");
const validate = sectionBetween(workflow, " validate:", " runtime_summary:");
expect(workflow).toContain("ci_mode:");
expect(scopes).toContain("ci_mode: ${{ steps.detect.outputs.ci_mode }}");
expect(scopes).toContain("ui_p0_validation_required: ${{ steps.detect.outputs.ui_p0_validation_required }}");
expect(scopes).toContain("run_ui_p0: ${{ steps.detect.outputs.run_ui_p0 }}");
expect(workflow).toContain("needs.scopes.outputs.run_ui_p0 == 'true'");
expect(validate).toContain('when($out.run_ui_p0 == "true"; ["ui_p0"])');
expect(plan).toContain("run: ${{ steps.hash.outputs.run }}");
expect(plan).toContain("scopes: ${{ steps.scopes.outputs.scopes }}");
expect(workflow).toContain("fromJSON(needs.plan.outputs.run).ui_p0");
expect(validate).toContain("[$run | to_entries[] | select(.value) | .key]");
await expect(runScopesPrint("workflow_dispatch", { inputs: { ci_mode: "hot" } }, ["apps/web/src/app/page.tsx"])).resolves.toMatchObject({
ci_mode: "hot",
@@ -1126,12 +1118,12 @@ process.stdin.on("end", () => {
const workflow = await readFile(ciWorkflowPath, "utf8");
const workspaceUnit = sectionBetween(workflow, " workspace_unit_tests:", " daemon_unit_tests:");
expect(workspaceUnit).toContain(`if [ "\${{ needs.scopes.outputs.tools_pack_tests_required }}" = "true" ]; then
expect(workspaceUnit).toContain(`if [ "\${{ fromJSON(needs.plan.outputs.scopes).tools_pack_tests_required }}" = "true" ]; then
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/desktop test
pnpm --filter @open-design/packaged test
pnpm --filter @open-design/tools-pack test
if [ "\${{ needs.scopes.outputs.run_e2e_vitest }}" != "true" ]; then
if [ "\${{ fromJSON(needs.plan.outputs.run).e2e_vitest }}" != "true" ]; then
pnpm --filter @open-design/e2e test tests/packaged-launcher-update-loop.test.ts
fi
fi`);
@@ -1142,12 +1134,12 @@ process.stdin.on("end", () => {
const daemonTests = sectionBetween(workflow, " daemon_unit_tests:", " windows_tools_pack_payload_tests:");
const validate = sectionBetween(workflow, " validate:", " runtime_summary:");
expect(daemonTests).toContain("if: ${{ needs.scopes.outputs.daemon_tests_required == 'true' }}");
expect(daemonTests).toContain("if: ${{ fromJSON(needs.plan.outputs.run).daemon_unit_tests }}");
expect(daemonTests).toContain("fail-fast: false");
expect(daemonTests).toContain("shard: [1, 2, 3, 4]");
expect(daemonTests).toContain("pnpm --filter @open-design/daemon test --shard=${{ matrix.shard }}/4");
expect(validate).toContain("- daemon_unit_tests");
expect(validate).toContain('when($out.daemon_tests_required == "true"; ["daemon_unit_tests"])');
expect(validate).toContain("[$run | to_entries[] | select(.value) | .key]");
});
it("[P2] skips the critical fallback for pure packaged-leaf changes and stays fail-closed elsewhere", async () => {
@@ -1262,32 +1254,66 @@ process.stdin.on("end", () => {
expect(validate).not.toContain("run_docker_build");
expect(validate).toContain("Check workspace validation jobs");
const baseOutputs = {
run_preflight: "false",
run_workspace_unit_tests: "false",
run_windows_tools_pack_payload_tests: "false",
run_web_workspace_tests: "false",
run_e2e_vitest: "false",
run_playwright_critical: "false",
run_ui_p0: "false",
run_playwright_visual: "false",
const baseRun = {
static_gate: false,
preflight: false,
workspace_unit_tests: false,
daemon_unit_tests: false,
windows_tools_pack_payload_tests: false,
web_workspace_tests: false,
e2e_vitest: false,
playwright_critical: false,
ui_p0: false,
playwright_visual: false,
};
// Core gate only cares about app jobs — unknown packaging keys are ignored.
await expect(
validateGatePasses(workflow, {
scopes: { result: "success", outputs: baseOutputs },
static_gate: { result: "success" },
plan: { result: "success", outputs: { run: JSON.stringify(baseRun) } },
}),
).resolves.toBe(true);
const needsWithFailedWeb = {
scopes: { result: "success", outputs: { ...baseOutputs, run_web_workspace_tests: "true" } },
static_gate: { result: "success" },
plan: { result: "success", outputs: { run: JSON.stringify({ ...baseRun, web_workspace_tests: true }) } },
web_workspace_tests: { result: "failure" },
};
await expect(validateGatePasses(workflow, needsWithFailedWeb)).resolves.toBe(false);
});
it("[P1] publishes hash state only after the workspace gate succeeds", async () => {
const workflow = await readFile(ciWorkflowPath, "utf8");
const plan = sectionBetween(workflow, " plan:", " static_gate:");
const validate = sectionBetween(workflow, " validate:", " runtime_summary:");
expect(plan).toContain("Upload pending hash map");
expect(plan).not.toContain("actions/cache/save");
expect(validate).toContain("Download pending hash map");
expect(validate).toContain("Save successful hash map");
expect(validate.indexOf("Save successful hash map")).toBeGreaterThan(
validate.indexOf("Check workspace validation jobs"),
);
expect(validate.indexOf("Save successful hash map")).toBeGreaterThan(
validate.indexOf("Block merge while a merge-blocking label is present"),
);
const run = {
static_gate: false,
preflight: false,
workspace_unit_tests: false,
daemon_unit_tests: false,
windows_tools_pack_payload_tests: false,
web_workspace_tests: false,
e2e_vitest: true,
playwright_critical: false,
ui_p0: false,
playwright_visual: false,
};
await expect(validateGatePasses(workflow, {
plan: { result: "success", outputs: { run: JSON.stringify(run) } },
e2e_vitest: { result: "failure" },
})).resolves.toBe(false);
});
it("[P1] includes launcher protocol in the Nix daemon workspace build", async () => {
const flake = await readFile(flakePath, "utf8");
const daemonWorkspaces = sectionBetween(flake, " daemonWorkspacePaths = [", " ];");
@@ -1300,8 +1326,8 @@ process.stdin.on("end", () => {
it("[P2] routes trusted Linux CI through the Nexu runner fleet", async () => {
const workflow = await readFile(ciWorkflowPath, "utf8");
const runners = sectionBetween(workflow, " runners:", " scopes:");
const scopes = sectionBetween(workflow, " scopes:", " static_gate:");
const runners = sectionBetween(workflow, " runners:", " plan:");
const plan = sectionBetween(workflow, " plan:", " static_gate:");
const staticGate = sectionBetween(workflow, " static_gate:", " preflight:");
const workspaceUnitTests = sectionBetween(workflow, " workspace_unit_tests:", " daemon_unit_tests:");
const daemonUnitTests = sectionBetween(workflow, " daemon_unit_tests:", " windows_tools_pack_payload_tests:");
@@ -1316,9 +1342,9 @@ process.stdin.on("end", () => {
expect(runners).toContain("runs_on: ${{ steps.runners.outputs.runs_on }}");
expect(runners).toContain("decision: ${{ steps.runners.outputs.decision }}");
expect(runners).toContain("python3 .github/scripts/runners.py");
expect(scopes).toContain("needs: [runners]");
expect(scopes).toContain("fromJSON(needs.runners.outputs.runs_on).control");
expect(staticGate).toContain("needs: [runners]");
expect(plan).toContain("needs: [runners]");
expect(plan).toContain("fromJSON(needs.runners.outputs.runs_on).control");
expect(staticGate).toContain("needs: [plan, runners]");
expect(staticGate).toContain("fromJSON(needs.runners.outputs.runs_on).control");
expect(workspaceUnitTests).toContain("fromJSON(needs.runners.outputs.runs_on).workspace_unit");
expect(workspaceUnitTests).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).workspace_unit)");
@@ -1345,7 +1371,7 @@ process.stdin.on("end", () => {
expect(uiP0).toContain(
"toJSON(matrix.shard == 'project-collab' && fromJSON(needs.runners.outputs.runs_on).ui_p0_heavy || fromJSON(needs.runners.outputs.runs_on).ui_p0)",
);
expect(uiP0).toContain("include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}");
expect(uiP0).toContain("include: ${{ fromJSON(needs.plan.outputs.ui_p0_matrix) }}");
expect(uiP0CiMatrix.map((entry) => entry.name)).toEqual([
"entry-settings",
"project-workspace",
@@ -1453,7 +1479,7 @@ process.stdin.on("end", () => {
it("[P1] routes external fork PRs through GitHub-hosted runner profiles", async () => {
const workflow = await readFile(ciWorkflowPath, "utf8");
const runners = sectionBetween(workflow, " runners:", " scopes:");
const runners = sectionBetween(workflow, " runners:", " plan:");
expect(runners).toContain("github.event_name == 'pull_request'");
expect(runners).toContain("github.event.pull_request.head.repo.full_name != github.repository");
@@ -1628,7 +1654,7 @@ process.stdin.on("end", () => {
const performanceProfiles = await runRunners("performance");
const performanceRunsOn = runnerRunsOn(performanceProfiles);
expect(runnerDecision(performanceProfiles)).toEqual({ schema_version: 1, mode: "performance" });
expect(runnerDecision(performanceProfiles)).toEqual({ schema_version: 1, mode: "default" });
expect(performanceRunsOn.control).toEqual(["nexu-runners-small"]);
expect(performanceRunsOn.general_medium).toEqual(["nexu-runners-medium"]);
expect(performanceRunsOn.workspace_unit).toEqual(["nexu-runners-medium"]);
@@ -1666,9 +1692,7 @@ process.stdin.on("end", () => {
expect(economicRunsOn.visual_hot).toEqual(["ubuntu-24.04"]);
for (const invalidMode of ["Economic", " economic "]) {
const fallbackProfiles = await runRunners(invalidMode);
expect(runnerDecision(fallbackProfiles)).toEqual({ schema_version: 1, mode: "default" });
expect(runnerRunsOn(fallbackProfiles).control).toEqual(["nexu-runners-small"]);
await expect(runRunners(invalidMode)).rejects.toThrow("unknown runner mode");
}
});
+2 -7
View File
@@ -18,16 +18,14 @@ describe("scripts guard library", () => {
expect(scriptsArchitectureErrors(sources)).toEqual([]);
});
test("scope startup cannot reach guard internals or package dependencies", () => {
test("root scripts cannot bypass the public guard entrypoint", () => {
const sources = new Map([
["scripts/scopes.ts", 'import "./lib/guard/core.ts";\nimport "typescript";'],
["scripts/check.ts", 'import "./lib/guard/core.ts";'],
["scripts/lib/guard/core.ts", ""],
["scripts/guard.ts", 'import "./lib/guard/core.ts";'],
["e2e/lib/playwright/suites.ts", ""],
]);
expect(scriptsArchitectureErrors(sources)).toEqual(
expect.arrayContaining([
expect.stringContaining("install-independent scope closure"),
expect.stringContaining("must consume guard policy through scripts/guard.ts"),
]),
);
@@ -35,11 +33,9 @@ describe("scripts guard library", () => {
test("guard internals cannot reverse-depend on the CLI or form cycles", () => {
const sources = new Map([
["scripts/scopes.ts", 'import "../e2e/lib/playwright/suites.ts";'],
["scripts/guard.ts", 'import "./lib/guard/core.ts";'],
["scripts/lib/guard/core.ts", 'import "./architecture.ts";'],
["scripts/lib/guard/architecture.ts", 'import "./core.ts";\nimport "../../guard.ts";\nprocess.exitCode = 1;'],
["e2e/lib/playwright/suites.ts", ""],
]);
const errors = scriptsArchitectureErrors(sources);
expect(errors).toEqual(
@@ -78,6 +74,5 @@ describe("scripts guard library", () => {
encoding: "utf8",
}).trim().split("\n");
expect(names).toContain("scripts library architecture");
expect(names).toContain("daemon core boundary");
});
});
+107
View File
@@ -0,0 +1,107 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, test } from "vitest";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const hashScript = path.join(repoRoot, ".github/scripts/hash.py");
const temporaryRoots: string[] = [];
function createRepository() {
const root = mkdtempSync(path.join(tmpdir(), "hash-contract-"));
temporaryRoots.push(root);
for (const [name, content] of [["control.txt", "control"], ["a.txt", "a"], ["b.txt", "b"]] as const) {
writeFileSync(path.join(root, name), content);
}
execFileSync("git", ["init", "-q"], { cwd: root });
execFileSync("git", ["add", "."], { cwd: root });
const configPath = path.join(root, "hash.json");
writeFileSync(configPath, JSON.stringify({
schema: { version: 1 },
suites: { "ci-control": ["control.txt"], web: ["a.txt"] },
workflows: { ci: { a: ["suite://web"], b: ["key://ci/a", "b.txt"], all: ["*"] } },
}));
const scopePlanPath = path.join(root, "scope-plan.json");
writeFileSync(scopePlanPath, JSON.stringify({ enabled: { a: true, b: true, all: true } }));
return { root, configPath, scopePlanPath, statePath: path.join(root, "state.json") };
}
function runHash(fixture: ReturnType<typeof createRepository>) {
const outputPath = path.join(fixture.root, "github-output.txt");
writeFileSync(outputPath, "");
const stdout = execFileSync("python3", [
hashScript, "--root", fixture.root, "--config", fixture.configPath,
"github-output", "--workflow", "ci", "--scope-plan", fixture.scopePlanPath, "--state", fixture.statePath,
], { cwd: fixture.root, encoding: "utf8", env: { ...process.env, GITHUB_OUTPUT: outputPath } });
return {
decision: JSON.parse(stdout) as { run: Record<string, boolean>; equal: Record<string, boolean> },
state: JSON.parse(readFileSync(fixture.statePath, "utf8")) as Record<string, unknown>,
};
}
afterEach(() => {
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe("hash input register", () => {
test("starts cold, compares hot, and rewrites only the current map", () => {
const fixture = createRepository();
expect(runHash(fixture).decision.run).toEqual({ a: true, all: true, b: true });
const hot = runHash(fixture);
expect(hot.decision.run).toEqual({ a: false, all: false, b: false });
expect(hot.state).toEqual({
schemaVersion: 1,
workflow: "ci",
hashes: expect.objectContaining({ a: expect.any(String), all: expect.any(String), b: expect.any(String) }),
});
});
test("composes suite and key dependencies while star observes the whole tree", () => {
const fixture = createRepository();
runHash(fixture);
writeFileSync(path.join(fixture.root, "b.txt"), "b2");
execFileSync("git", ["add", "b.txt"], { cwd: fixture.root });
expect(runHash(fixture).decision.run).toEqual({ a: false, all: true, b: true });
writeFileSync(path.join(fixture.root, "a.txt"), "a2");
execFileSync("git", ["add", "a.txt"], { cwd: fixture.root });
expect(runHash(fixture).decision.run).toEqual({ a: true, all: true, b: true });
});
test("corrupt or missing state fails cold without weakening the plan", () => {
const fixture = createRepository();
runHash(fixture);
writeFileSync(fixture.statePath, "not json");
expect(runHash(fixture).decision.run).toEqual({ a: true, all: true, b: true });
});
test("rejects cycles through the implicit control closure before evaluation", () => {
const fixture = createRepository();
writeFileSync(fixture.configPath, JSON.stringify({
schema: { version: 1 },
suites: { "ci-control": ["key://ci/a"] },
workflows: { ci: { a: ["a.txt"] } },
}));
const failed = spawnSync("python3", [hashScript, "--root", fixture.root, "--config", fixture.configPath, "validate"], {
cwd: fixture.root,
encoding: "utf8",
});
expect(failed.status).toBe(2);
expect(failed.stderr).toContain("hash dependency cycle");
writeFileSync(fixture.configPath, JSON.stringify({
schema: { version: 1 },
suites: { "ci-control": ["control.txt"] },
workflows: { ci: { a: ["suite://missing"] } },
}));
const dangling = spawnSync("python3", [hashScript, "--root", fixture.root, "--config", fixture.configPath, "validate"], {
cwd: fixture.root,
encoding: "utf8",
});
expect(dangling.status).toBe(2);
expect(dangling.stderr).toContain("references unknown suite://missing");
});
});
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -40,16 +40,15 @@ import { describe, expect, it } from 'vitest';
* incidental:
*
* - Any change under `apps/daemon/src/` matches the `certain-daemon-core` rule
* in `scripts/scopes.ts`, whose effects include `ui_p0_validation_required`;
* in `.github/config/scopes.json`, whose effects include `ui_p0_validation_required`;
* `run_e2e_vitest` is `isFull || web_tests_required ||
* ui_p0_validation_required`. So a `server.ts`-only change arms the `E2E
* Vitest` lane even at the merge queue's `certain` threshold — the strictest
* context there is. An unresolved file list escalates fail-closed to full,
* which arms it too.
* - That wiring cannot rot silently. The `daemon core boundary` guard
* (`scripts/lib/guard/scope.ts`) asserts `ci.yml` still contains both
* `run_e2e_vitest == 'true'` and `pnpm --filter @open-design/e2e test`, and
* it runs in the always-on policy floor.
* - Direct `scopes.py plan` tests pin that routing, while the workflow topology
* test pins the `e2e_vitest` hash-run identity and package command. Those are
* planner contract tests, not an independent proof that authorizes the plan.
*
* The placement also follows the root `AGENTS.md` boundary rule cross-app and
* repository-resource consistency checks belong here, not inside an app package
-307
View File
@@ -1,307 +0,0 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import ts from "typescript";
import {
CERTAIN_DAEMON_CORE_EXACT,
CERTAIN_EXEMPT_EXACT,
CERTAIN_EXEMPT_PREFIXES,
} from "./scopes.ts";
// Guard for the certain-tier exempt core in `scripts/scopes.ts` (rule
// `certain-exempt-surface`; methodology in `specs/current/ci.md`).
//
// Boundary invariant: no source that a *skippable* merge-gate lane executes may
// consume a certain-exempt file. If that held false, a docs-only change could
// invalidate a lane the promoted rule tells the merge queue to skip.
//
// What "consumption" means here, at two precision levels:
// - a dot-relative literal (`../../docs/...`) that resolves from the file's
// repo location into the certain-exempt surface: flagged everywhere — it can
// only mean the repository surface.
// - a bare repo-relative literal (`docs/CHANGELOG`): flagged everywhere unless
// it is an argument to a known sandbox-fixture writer. Test files can also
// contain repo-root helpers, so exempting them wholesale would hide real
// consumption.
// - path.join/path.resolve expressions made from static segments (optionally
// anchored at repoRoot), and template literals whose static prefix already
// enters an exempt directory: flagged as the same repository dependency.
//
// Deliberately outside the checked surface:
// - root `scripts/` — policy-floor code. Preflight is armed on every plan, so
// its guard checks may read the exempt surface (product neutrality validates
// docs/ prose on every run).
// - `apps/landing-page/` — it IS the exempt surface; landing-page CI owns it.
const repoRoot = path.resolve(import.meta.dirname, "..");
const checkedRoots = ["apps", "packages", "tools", "e2e"] as const;
const skippedDirectoryNames = new Set([
".astro",
".next",
".od-data",
"dist",
"node_modules",
"out",
"reports",
"test-results",
"vendor",
]);
const skippedRepositoryPrefixes = ["apps/landing-page/"];
const checkedExtensions = new Set([".ts", ".tsx"]);
// These helpers interpret their path argument inside a temporary project
// workspace. Keep this list narrow: repo-root readers used by tests must remain
// visible to the guard.
const sandboxFixtureWriterNames = new Set(["writeProjectFile"]);
// File-level exceptions. Every entry must explain why the reference is not
// gate-lane consumption of exempt-file *content*; revisit the entry if the
// file's relationship to the exempt surface changes.
const allowedConsumers = new Map<string, string>([
[
"apps/daemon/tests/claude-design-import.test.ts",
"archive-entry and extracted-project fixture paths; every docs/ literal names data inside a temporary project",
],
[
"apps/daemon/tests/design-systems/file-score.test.ts",
"path-classification fixture strings; the test does not open the editor configuration files",
],
[
"apps/daemon/tests/project-classifiers.test.ts",
"file-kind classifier input; the LICENSE literal is never resolved or opened",
],
[
"apps/web/tests/components/ChatPane.imported-folder-artifacts.test.tsx",
"imported-project artifact fixture paths rendered from in-memory test data",
],
[
"apps/web/tests/components/file-viewer-markdown-copy.test.tsx",
"project-relative markdown path inputs used to test URL construction, not repository reads",
],
[
"apps/web/tests/utils/inlineMentions.test.ts",
"in-memory mention parser fixture paths; no filesystem access occurs",
],
[
"tools/release/src/release-note/prepare.ts",
"docs/CHANGELOG feeds release-note preparation, which runs only in release workflows; @open-design/tools-release tests run in no ci.yml lane",
],
[
"e2e/tests/packaged-smoke-workflow.test.ts",
"scope-planner and workflow-text assertion fixtures; certain-exempt literals are passed as data and never opened",
],
[
"e2e/tests/scripts/product-neutrality.test.ts",
"virtual source path passed to the product-neutrality collector; it does not access the repository file",
],
[
"e2e/tests/scripts/scopes.test.ts",
"behavior fixtures for this very check: source snippets passed to the collector as data, never resolved or opened",
],
]);
// Content dependencies whose producers are classified into the same certain
// lane as their consumers. Unlike allowedConsumers, these exceptions are
// exact producer/consumer pairs so another exempt read in the same file still
// fails closed.
const allowedConsumerTargets = new Map<string, ReadonlyMap<string, string>>([
[
"apps/daemon/tests/runtimes/trae-cli.test.ts",
new Map([
[
CERTAIN_DAEMON_CORE_EXACT[0],
"the exact consumed document is daemon core, so producer and consumer run the same suite",
],
]),
],
]);
type ConsumptionViolation = {
filePath: string;
lineNumber: number;
literal: string;
repositoryTarget: string;
};
function toRepositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
function landsInCertainExemptSurface(repositoryPath: string): boolean {
return (
CERTAIN_EXEMPT_PREFIXES.some((prefix) => repositoryPath.startsWith(prefix)) ||
(CERTAIN_EXEMPT_EXACT as readonly string[]).includes(repositoryPath)
);
}
function literalConsumesCertainExemptSurface(fromRepositoryPath: string, literal: string): boolean {
if (literal.startsWith("./") || literal.startsWith("../")) {
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(fromRepositoryPath), literal));
return landsInCertainExemptSurface(resolved);
}
return landsInCertainExemptSurface(literal);
}
function isSandboxFixtureWriterArgument(node: ts.Expression): boolean {
const call = node.parent;
if (!ts.isCallExpression(call) || !call.arguments.includes(node)) return false;
const callee = call.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
return name !== undefined && sandboxFixtureWriterNames.has(name);
}
type StaticPath = {
path: string;
display: string;
prefixOnly: boolean;
};
function staticPathFromExpression(node: ts.Expression, sourceFile: ts.SourceFile): StaticPath | undefined {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return { path: node.text, display: node.text, prefixOnly: false };
}
if (ts.isTemplateExpression(node)) {
const prefix = node.head.text;
if (!CERTAIN_EXEMPT_PREFIXES.some((exemptPrefix) => prefix.startsWith(exemptPrefix))) {
return undefined;
}
return { path: prefix, display: node.getText(sourceFile), prefixOnly: true };
}
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return undefined;
const callee = node.expression;
if (
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "path" ||
(callee.name.text !== "join" && callee.name.text !== "resolve")
) {
return undefined;
}
const segments: string[] = [];
for (const [index, argument] of node.arguments.entries()) {
if (index === 0 && ts.isIdentifier(argument) && argument.text === "repoRoot") continue;
const segment = staticPathFromExpression(argument, sourceFile);
if (segment === undefined || segment.prefixOnly) return undefined;
segments.push(segment.path);
}
if (segments.length === 0) return undefined;
return {
path: path.posix.join(...segments),
display: node.getText(sourceFile),
prefixOnly: false,
};
}
export function collectCertainExemptConsumptionFromSource(
repositoryPath: string,
source: string,
): ConsumptionViolation[] {
const sourceFile = ts.createSourceFile(
repositoryPath,
source,
ts.ScriptTarget.Latest,
true,
repositoryPath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
const violations: ConsumptionViolation[] = [];
const visit = (node: ts.Node): void => {
if (ts.isExpression(node)) {
const candidate = staticPathFromExpression(node, sourceFile);
if (candidate !== undefined && !isSandboxFixtureWriterArgument(node)) {
const consumes = candidate.prefixOnly
? CERTAIN_EXEMPT_PREFIXES.some((prefix) => candidate.path.startsWith(prefix))
: literalConsumesCertainExemptSurface(repositoryPath, candidate.path);
if (consumes) {
violations.push({
filePath: repositoryPath,
lineNumber: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
literal: candidate.display,
repositoryTarget: candidate.path,
});
return;
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return violations;
}
export function collectDisallowedCertainExemptConsumptionFromSource(
repositoryPath: string,
source: string,
): ConsumptionViolation[] {
if (allowedConsumers.has(repositoryPath)) return [];
const allowedTargets = allowedConsumerTargets.get(repositoryPath);
return collectCertainExemptConsumptionFromSource(repositoryPath, source).filter(
(violation) => !allowedTargets?.has(violation.repositoryTarget),
);
}
async function collectCheckedFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
const repositoryPath = toRepositoryPath(fullPath);
if (entry.isDirectory()) {
if (
skippedDirectoryNames.has(entry.name) ||
entry.name.startsWith(".next-") ||
skippedRepositoryPrefixes.some((prefix) => `${repositoryPath}/`.startsWith(prefix))
) {
continue;
}
files.push(...(await collectCheckedFiles(fullPath)));
continue;
}
if (entry.isFile() && checkedExtensions.has(path.extname(entry.name))) {
files.push(repositoryPath);
}
}
return files;
}
export async function checkCertainExemptConsumption(): Promise<boolean> {
const violations: ConsumptionViolation[] = [];
for (const root of checkedRoots) {
for (const repositoryPath of await collectCheckedFiles(path.join(repoRoot, root))) {
const source = await readFile(path.join(repoRoot, repositoryPath), "utf8");
violations.push(...collectDisallowedCertainExemptConsumptionFromSource(repositoryPath, source));
}
}
if (violations.length > 0) {
console.error("Certain-exempt surface consumption found in gate-lane sources:");
for (const violation of violations) {
console.error(`- ${violation.filePath}:${violation.lineNumber} \`${violation.literal}\``);
}
console.error(
"Certain-tier exempt files must stay unconsumed by skippable merge-gate lanes (see specs/current/ci.md). Move the dependency, or add a justified allowlist entry in scripts/check-certain-exempt-consumption.ts.",
);
return false;
}
console.log("Certain-exempt consumption check passed: gate-lane sources do not read the certain-exempt surface.");
return true;
}
-268
View File
@@ -1,268 +0,0 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import ts from "typescript";
import {
CERTAIN_PACKAGED_LEAF_PREFIXES,
evaluateScopeOutputs,
SCOPE_EFFECTS,
} from "./scopes.ts";
const repoRoot = path.resolve(import.meta.dirname, "..");
const checkedRoots = ["apps", "packages", "tools", "e2e"] as const;
const checkedExtensions = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
const skippedDirectories = new Set([
".astro",
".next",
".od-data",
"dist",
"node_modules",
"out",
"reports",
"test-results",
"vendor",
]);
const leafPackages = ["@open-design/desktop", "@open-design/packaged", "@open-design/tools-pack"] as const;
const allowedConsumerPrefixes = new Map([
[
"tools/dev/",
"tools-dev launches the desktop entry; the certain rule keeps @open-design/tools-dev tests armed",
],
]);
const allowedConsumers = new Map([
[
"apps/packaged/esbuild.config.mjs",
"the packaged build owns these entrypoints; the config itself remains medium-tier",
],
[
"tools/pack/esbuild.config.mjs",
"the tools-pack build owns this entrypoint; the config itself remains medium-tier",
],
[
"e2e/tests/packaged-launcher-update-loop.test.ts",
"the focused cross-boundary fallback runs whenever broad E2E Vitest is skipped",
],
[
"e2e/tests/packaged-smoke-workflow.test.ts",
"workflow and scope topology fixtures; packaged paths are data rather than runtime imports",
],
[
"e2e/tests/scripts/approve-fork-pr-workflows.test.ts",
"fork-approval path classification fixtures; packaged paths are data",
],
[
"e2e/tests/scripts/check-cross-app-imports.test.ts",
"cross-app import parser fixtures; the packaged import is passed as source text",
],
[
"e2e/tests/scripts/scopes.test.ts",
"scope planner fixtures; packaged paths are classification inputs",
],
[
"packages/platform/tests/index.test.ts",
"package-manager invocation fixtures serialize the desktop build command without loading desktop code",
],
]);
const requiredWorkspaceUnitBlock = `if [ "\${{ needs.scopes.outputs.tools_dev_tests_required }}" = "true" ]; then
pnpm --filter @open-design/tools-dev test
fi
if [ "\${{ needs.scopes.outputs.tools_pack_tests_required }}" = "true" ]; then
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/desktop test
pnpm --filter @open-design/packaged test
pnpm --filter @open-design/tools-pack test
if [ "\${{ needs.scopes.outputs.run_e2e_vitest }}" != "true" ]; then
pnpm --filter @open-design/e2e test tests/packaged-launcher-update-loop.test.ts
fi
fi`;
export type PackagedLeafConsumptionViolation = {
filePath: string;
lineNumber: number;
literal: string;
};
function repositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
function landsInPackagedLeaf(value: string): boolean {
return CERTAIN_PACKAGED_LEAF_PREFIXES.some(
(prefix) => value === prefix.slice(0, -1) || value.startsWith(prefix),
);
}
function literalTargetsPackagedLeaf(fromRepositoryPath: string, literal: string): boolean {
if (leafPackages.some((packageName) => literal === packageName || literal.startsWith(`${packageName}/`))) {
return true;
}
const resolved =
literal.startsWith("./") || literal.startsWith("../")
? path.posix.normalize(path.posix.join(path.posix.dirname(fromRepositoryPath), literal))
: literal;
return landsInPackagedLeaf(resolved);
}
type StaticPath = { value: string; display: string };
function staticPath(node: ts.Expression, sourceFile: ts.SourceFile): StaticPath | undefined {
if (ts.isStringLiteralLike(node)) return { value: node.text, display: node.getText(sourceFile) };
if (ts.isTemplateExpression(node)) {
return { value: node.head.text, display: node.getText(sourceFile) };
}
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return undefined;
if (
!ts.isIdentifier(node.expression.expression) ||
node.expression.expression.text !== "path" ||
(node.expression.name.text !== "join" && node.expression.name.text !== "resolve")
) {
return undefined;
}
const segments: string[] = [];
for (const [index, argument] of node.arguments.entries()) {
if (index === 0 && ts.isIdentifier(argument) && /^(?:repoRoot|workspaceRoot|WORKSPACE_ROOT)$/.test(argument.text)) {
continue;
}
if (!ts.isExpression(argument)) return undefined;
const segment = staticPath(argument, sourceFile);
if (segment == null || segment.value.length === 0) return undefined;
segments.push(segment.value);
}
return segments.length > 0
? { value: path.posix.join(...segments), display: node.getText(sourceFile) }
: undefined;
}
export function collectPackagedLeafConsumptionFromSource(
filePath: string,
source: string,
): PackagedLeafConsumptionViolation[] {
const sourceFile = ts.createSourceFile(
filePath,
source,
ts.ScriptTarget.Latest,
true,
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
const violations: PackagedLeafConsumptionViolation[] = [];
const seen = new Set<number>();
const visit = (node: ts.Node): void => {
if (ts.isExpression(node)) {
const candidate = staticPath(node, sourceFile);
if (candidate != null && literalTargetsPackagedLeaf(filePath, candidate.value)) {
const start = node.getStart(sourceFile);
if (!seen.has(start)) {
seen.add(start);
violations.push({
filePath,
lineNumber: sourceFile.getLineAndCharacterOfPosition(start).line + 1,
literal: candidate.display,
});
}
return;
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return violations;
}
function isAllowedConsumer(filePath: string): boolean {
return (
allowedConsumers.has(filePath) ||
[...allowedConsumerPrefixes.keys()].some((prefix) => filePath.startsWith(prefix))
);
}
async function collectCheckedFiles(directory: string): Promise<string[]> {
const files: string[] = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const fullPath = path.join(directory, entry.name);
const repoPath = repositoryPath(fullPath);
if (entry.isDirectory()) {
if (
skippedDirectories.has(entry.name) ||
entry.name.startsWith(".next-") ||
landsInPackagedLeaf(`${repoPath}/`)
) {
continue;
}
files.push(...(await collectCheckedFiles(fullPath)));
} else if (entry.isFile() && checkedExtensions.has(path.extname(entry.name))) {
files.push(repoPath);
}
}
return files;
}
function scopeBoundaryErrors(): string[] {
const expectedEffects = new Set([
"tools_dev_tests_required",
"tools_pack_tests_required",
"workspace_validation_required",
]);
const errors: string[] = [];
for (const filePath of [
"apps/desktop/src/main/index.ts",
"apps/desktop/tests/main/runtime.test.ts",
"apps/packaged/src/index.ts",
"apps/packaged/tests/index.test.ts",
"tools/pack/src/index.ts",
"tools/pack/tests/index.test.ts",
"tools/pack/resources/linux/open-design.desktop.template",
]) {
const evaluation = evaluateScopeOutputs([filePath], "certain", {
deriveWorkspaceValidationFromTestScopes: true,
});
const decision = evaluation.decisions[0];
const actualEffects = SCOPE_EFFECTS.filter((effect) => evaluation.outputs[effect]);
if (
decision?.escalated !== false ||
decision.matchedRules.length !== 1 ||
decision.matchedRules[0] !== "certain-packaged-leaf-sources" ||
actualEffects.length !== expectedEffects.size ||
actualEffects.some((effect) => !expectedEffects.has(effect))
) {
errors.push(`${filePath} does not resolve to the guarded packaged-leaf effects at the certain threshold`);
}
}
return errors;
}
export async function checkPackagedLeafBoundary(): Promise<boolean> {
const violations: PackagedLeafConsumptionViolation[] = [];
for (const root of checkedRoots) {
for (const filePath of await collectCheckedFiles(path.join(repoRoot, root))) {
if (isAllowedConsumer(filePath)) continue;
const source = await readFile(path.join(repoRoot, filePath), "utf8");
violations.push(...collectPackagedLeafConsumptionFromSource(filePath, source));
}
}
const workflow = await readFile(path.join(repoRoot, ".github/workflows/ci.yml"), "utf8");
const errors = scopeBoundaryErrors();
if (!workflow.includes(requiredWorkspaceUnitBlock)) {
errors.push("ci.yml no longer contains the guarded tools-dev, packaged unit, and focused E2E command block");
}
if (violations.length > 0 || errors.length > 0) {
console.error("Packaged-leaf boundary violations found:");
for (const violation of violations) {
console.error(`- ${violation.filePath}:${violation.lineNumber} ${violation.literal}`);
}
for (const error of errors) console.error(`- ${error}`);
console.error("Keep new consumers inside the guarded validation plan or leave the changed surface at medium confidence.");
return false;
}
console.log("Packaged-leaf boundary check passed: certain-tier consumers, effects, and CI commands stay aligned.");
return true;
}
+1 -54
View File
@@ -3,9 +3,7 @@ import path from "node:path";
import { pathToFileURL } from "node:url";
import ts from "typescript";
import { checkCertainExemptConsumption } from "./check-certain-exempt-consumption.ts";
import { checkCrossAppImports } from "./check-cross-app-imports.ts";
import { checkPackagedLeafBoundary } from "./check-packaged-leaf-boundary.ts";
import { checkTsNocheckImports } from "./check-ts-nocheck-imports.ts";
import { checkDesignSystemManifests } from "./check-design-system-manifests.ts";
import { checkDesignSystemPackageQuality } from "./check-design-system-package-quality.ts";
@@ -13,7 +11,6 @@ import { checkDesignSystemComponentFixtureReport } from "./check-components-fixt
import { checkDesignSystemFlagParity } from "./check-design-system-flag-parity.ts";
import { checkComponentsManifestExtraction } from "./check-components-manifest-extraction.ts";
import { checkPluginPreviewManifest } from "./check-plugin-preview-manifest.ts";
import { validatePlaywrightSuiteTopology } from "../e2e/lib/playwright/suites.ts";
import {
checkDesignSystemA1RequiredTokens,
checkDesignSystemA2DefaultsParity,
@@ -26,10 +23,6 @@ import { checkCraftReferences } from "./lint-craft-references.ts";
import { collectCssHardcodedColorMatches, cssWideAndSpecialColorKeywords, realNamedColors } from "./style-policy.ts";
import { checkScriptsLibraryArchitecture } from "./lib/guard/architecture.ts";
import { runGuardChecks, type GuardCheck, type GuardContext } from "./lib/guard/core.ts";
import {
checkDaemonCoreBoundary as checkDaemonCoreScopeBoundary,
checkUiP0ShadowContract,
} from "./lib/guard/scope.ts";
const repoRoot = path.resolve(import.meta.dirname, "..");
const allowedE2eScripts = new Set([
@@ -1294,37 +1287,6 @@ async function checkStylePolicy(): Promise<boolean> {
return true;
}
async function checkCiTopology(): Promise<boolean> {
const ciWorkflow = await readFile(path.join(repoRoot, ".github/workflows/ci.yml"), "utf8");
const errors = [
...validatePlaywrightSuiteTopology(),
...[
"run: node --experimental-strip-types scripts/scopes.ts github-output",
"ci_mode: ${{ steps.detect.outputs.ci_mode }}",
"ui_p0_validation_required: ${{ steps.detect.outputs.ui_p0_validation_required }}",
"run_ui_p0: ${{ steps.detect.outputs.run_ui_p0 }}",
"ui_p0_matrix: ${{ steps.detect.outputs.ui_p0_matrix }}",
"visual_matrix: ${{ steps.detect.outputs.visual_matrix }}",
"include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}",
"include: ${{ fromJSON(needs.scopes.outputs.visual_matrix) }}",
"needs.scopes.outputs.run_ui_p0 == 'true'",
"pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group critical-extras",
"pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group ${{ matrix.shard }}",
]
.filter((needle) => !ciWorkflow.includes(needle))
.map((needle) => `.github/workflows/ci.yml is missing ${needle}`),
];
if (errors.length > 0) {
console.error("CI topology check failed:");
for (const error of errors) console.error(`- ${error}`);
return false;
}
console.log("CI topology check passed: scopes, Playwright suites, and workflow matrices stay aligned.");
return true;
}
let crossAppImportsResult: Promise<boolean> | undefined;
function checkCrossAppImportsOnce(): Promise<boolean> {
@@ -1332,20 +1294,8 @@ function checkCrossAppImportsOnce(): Promise<boolean> {
return crossAppImportsResult;
}
async function checkDaemonCoreBoundary(context: GuardContext): Promise<boolean> {
const [crossAppImportsPass, scopeBoundaryPass] = await Promise.all([
checkCrossAppImportsOnce(),
checkDaemonCoreScopeBoundary(context),
]);
return crossAppImportsPass && scopeBoundaryPass;
}
const checks: GuardCheck[] = [
{ name: "residual JavaScript", run: checkResidualJavaScript },
{ name: "certain-exempt surface consumption", run: checkCertainExemptConsumption },
{ name: "packaged leaf boundary", run: checkPackagedLeafBoundary },
{ name: "daemon core boundary", run: checkDaemonCoreBoundary },
{ name: "UI P0 shadow contract", run: checkUiP0ShadowContract },
{ name: "package dependency specs", run: checkPackageDependencySpecs },
{ name: "product neutrality", run: checkProductNeutrality },
{ name: "cross-app imports", run: checkCrossAppImportsOnce },
@@ -1358,7 +1308,6 @@ const checks: GuardCheck[] = [
{ name: "web import isolation", run: checkWebImportIsolation },
{ name: "tools layout", run: checkToolsLayout },
{ name: "style policy", run: checkStylePolicy },
{ name: "CI topology", run: checkCiTopology },
{ name: "craft references", run: checkCraftReferences },
{ name: "plugin preview manifest", run: checkPluginPreviewManifest },
{ name: "design system manifests", run: checkDesignSystemManifests },
@@ -1376,9 +1325,7 @@ const checks: GuardCheck[] = [
const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
if (isMain) {
// `--list-checks` is the machine-readable registry of guard check names; the
// scope rule-table invariant test resolves `certain` rules' guard fields
// against it so a renamed or deleted guard fails CI.
// `--list-checks` is the machine-readable registry of repository guard checks.
if (process.argv[2] === "--list-checks") {
for (const check of checks) console.log(check.name);
} else if (!(await runGuardChecks(checks, { repoRoot }))) {
+3 -39
View File
@@ -4,12 +4,8 @@ import ts from "typescript";
import type { GuardContext } from "./core.ts";
const suiteModule = "e2e/lib/playwright/suites.ts";
const scopesModule = "scripts/scopes.ts";
const guardModule = "scripts/guard.ts";
const guardLibraryPrefix = "scripts/lib/guard/";
const scopePolicyModule = `${guardLibraryPrefix}scope.ts`;
const scopeLibraryPrefix = "scripts/lib/scope/";
function repositoryPath(filePath: string): string {
return filePath.split(path.sep).join("/");
@@ -35,9 +31,7 @@ async function collectTypeScriptSources(
}
export async function loadScriptsArchitectureSources(repoRoot: string): Promise<Map<string, string>> {
const sources = await collectTypeScriptSources(repoRoot);
sources.set(suiteModule, await readFile(path.join(repoRoot, suiteModule), "utf8"));
return sources;
return collectTypeScriptSources(repoRoot);
}
function importsFrom(source: string): string[] {
@@ -129,12 +123,7 @@ export function scriptsArchitectureErrors(sources: ReadonlyMap<string, string>):
if (dependency != null && specifier.startsWith(".") && !sources.has(dependency)) {
errors.push(`${module} imports missing module ${dependency}`);
}
if (
dependency == null ||
(!dependency.startsWith(guardLibraryPrefix) &&
(module !== scopePolicyModule ||
(dependency !== scopesModule && dependency !== suiteModule)))
) {
if (dependency == null || !dependency.startsWith(guardLibraryPrefix)) {
errors.push(`${module} imports ${specifier} outside the guard library closure`);
}
if (dependency === guardModule) {
@@ -148,33 +137,8 @@ export function scriptsArchitectureErrors(sources: ReadonlyMap<string, string>):
}
}
const scopeClosure = new Set<string>();
const visitScope = (module: string): void => {
if (scopeClosure.has(module)) return;
scopeClosure.add(module);
const source = sources.get(module);
if (source == null) {
errors.push(`${module} is missing from the preinstall scope closure`);
return;
}
for (const specifier of importsFrom(source)) {
if (specifier.startsWith("node:")) continue;
const dependency = resolveImport(module, specifier, sources);
if (
dependency == null ||
(dependency !== suiteModule && !dependency.startsWith(scopeLibraryPrefix))
) {
errors.push(`${module} imports ${specifier} outside the install-independent scope closure`);
continue;
}
visitScope(dependency);
}
};
visitScope(scopesModule);
errors.push(
...cycleErrors(graph, [
scopesModule,
...[...sources.keys()].filter((module) => module.startsWith("scripts/lib/")),
]),
);
@@ -188,6 +152,6 @@ export async function checkScriptsLibraryArchitecture(context: GuardContext): Pr
for (const error of errors) console.error(`- ${error}`);
return false;
}
console.log("Scripts library architecture check passed: scope startup and guard internals stay layered.");
console.log("Scripts library architecture check passed: workflow control and guard internals stay layered.");
return true;
}
-228
View File
@@ -1,228 +0,0 @@
import { access, readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { uiP0CiMatrix } from "../../../e2e/lib/playwright/suites.ts";
import {
CERTAIN_DAEMON_CORE_EXCLUDED_EXACT,
CERTAIN_DAEMON_CORE_EXCLUDED_PREFIXES,
CERTAIN_DAEMON_CORE_PREFIXES,
DAEMON_RUNTIME_DEFINITION_EXACT,
DAEMON_RUNTIME_DEFINITION_PREFIXES,
evaluateScopeOutputs,
evaluateUiP0Shadow,
matchesRuleMatch,
scopeRules,
} from "../../scopes.ts";
import type { GuardContext } from "./core.ts";
const fullMatrixNames = [
"entry-settings",
"project-workspace",
"project-workspace-editor",
"project-collab",
"project-runtime",
"workspace-restoration",
] as const;
const candidateMatrixNames = [
"entry-settings",
"project-workspace",
"project-collab",
"project-runtime",
] as const;
function matrixNames(matrix: readonly { name: string }[]): string[] {
return matrix.map((entry) => entry.name);
}
function sameValues(actual: readonly string[], expected: readonly string[]): boolean {
return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
}
export function uiP0ShadowContractErrors(): string[] {
const errors: string[] = [];
if (!sameValues(matrixNames(uiP0CiMatrix), fullMatrixNames)) {
errors.push("the applied UI P0 matrix is no longer the guarded full six-domain matrix");
}
const sourceSample = `${DAEMON_RUNTIME_DEFINITION_PREFIXES[0]}example.ts`;
const testSample = DAEMON_RUNTIME_DEFINITION_EXACT.find((file) => file.includes("/tests/"));
const candidate = evaluateUiP0Shadow(testSample == null ? [sourceSample] : [sourceSample, testSample]);
if (
candidate.mode !== "candidate" ||
candidate.capability !== "daemon-runtime-definition" ||
!sameValues(matrixNames(candidate.matrix), candidateMatrixNames)
) {
errors.push("the runtime-definition shadow no longer resolves to the guarded four-domain candidate");
}
for (const outsideFile of [
"apps/daemon/src/server.ts",
"apps/daemon/src/runtimes/detection.ts",
"apps/web/src/App.tsx",
]) {
const fallback = evaluateUiP0Shadow([sourceSample, outsideFile]);
if (
fallback.mode !== "full-fallback" ||
fallback.reason !== "outside-capability" ||
!sameValues(matrixNames(fallback.matrix), fullMatrixNames)
) {
errors.push(`${outsideFile} no longer forces the runtime-definition shadow to the full matrix`);
}
}
const unresolved = evaluateUiP0Shadow([], false);
if (
unresolved.mode !== "full-fallback" ||
unresolved.reason !== "files-unresolved" ||
!sameValues(matrixNames(unresolved.matrix), fullMatrixNames)
) {
errors.push("unresolved changed files no longer force the UI P0 shadow to the full matrix");
}
return errors;
}
export async function checkUiP0ShadowContract(): Promise<boolean> {
const errors = uiP0ShadowContractErrors();
if (errors.length > 0) {
console.error("UI P0 shadow-contract violations found:");
for (const error of errors) console.error(`- ${error}`);
return false;
}
console.log("UI P0 shadow contract check passed: applied coverage stays full and fallbacks stay closed.");
return true;
}
const daemonCoreRuleId = "certain-daemon-core";
const daemonCoreEffects = [
"daemon_tests_required",
"ui_critical_validation_required",
"ui_p0_validation_required",
"workspace_validation_required",
] as const;
function matchingRuleIds(file: string): string[] {
return scopeRules.filter((rule) => matchesRuleMatch(file, rule.match)).map((rule) => rule.id);
}
export function daemonCoreScopeContractErrors(): string[] {
const errors: string[] = [];
const samples = [
`${CERTAIN_DAEMON_CORE_PREFIXES[0]}server.ts`,
`${CERTAIN_DAEMON_CORE_PREFIXES[0]}policy.md`,
`${CERTAIN_DAEMON_CORE_PREFIXES[1]}server.test.ts`,
];
for (const sample of samples) {
const matched = matchingRuleIds(sample);
if (!sameValues(matched, [daemonCoreRuleId])) {
errors.push(`${sample} resolves to ${matched.join(", ") || "no rules"} instead of only ${daemonCoreRuleId}`);
}
}
const evaluation = evaluateScopeOutputs(samples, "certain", {
deriveWorkspaceValidationFromTestScopes: true,
});
const enabledEffects = Object.entries(evaluation.outputs)
.filter(([, enabled]) => enabled)
.map(([effect]) => effect);
if (!sameValues(enabledEffects, daemonCoreEffects)) {
errors.push(`daemon core effects changed from ${daemonCoreEffects.join(", ")} to ${enabledEffects.join(", ")}`);
}
if (evaluation.decisions.some((decision) => decision.escalated)) {
errors.push("daemon core samples no longer resolve without certain-tier escalation");
}
for (const outsideFile of [
`${CERTAIN_DAEMON_CORE_EXCLUDED_PREFIXES[0]}server.ts`,
`${DAEMON_RUNTIME_DEFINITION_PREFIXES[0]}example.ts`,
CERTAIN_DAEMON_CORE_EXCLUDED_EXACT[0],
"apps/daemon/package.json",
]) {
const outside = evaluateScopeOutputs([outsideFile], "certain", {
deriveWorkspaceValidationFromTestScopes: true,
});
if (!outside.decisions[0]?.escalated || matchingRuleIds(outsideFile).includes(daemonCoreRuleId)) {
errors.push(`${outsideFile} no longer stays outside the certain daemon core`);
}
}
return errors;
}
async function webDaemonFilesystemConsumers(repoRoot: string): Promise<string[]> {
const consumers: string[] = [];
const root = path.join(repoRoot, "apps/web/tests");
const visit = async (directory: string): Promise<void> => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(fullPath);
} else if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name)) {
const source = await readFile(fullPath, "utf8");
const usesFileSystem = /from\s+["']node:fs(?:\/promises)?["']/.test(source);
const namesDaemonTree =
source.includes("apps/daemon/") ||
/["']apps["']\s*,\s*["']daemon["']/.test(source);
if (usesFileSystem && namesDaemonTree) {
consumers.push(path.relative(repoRoot, fullPath).split(path.sep).join("/"));
}
}
}
};
await visit(root);
return consumers.sort();
}
async function daemonCoreRepositoryContractErrors(repoRoot: string): Promise<string[]> {
const errors = daemonCoreScopeContractErrors();
const visualHarness = await readFile(path.join(repoRoot, "e2e/lib/playwright/visual.ts"), "utf8");
const ciWorkflow = await readFile(path.join(repoRoot, ".github/workflows/ci.yml"), "utf8");
for (const pattern of ["**/api/**", "**/artifacts/**", "**/frames/**", "**/powered/**"]) {
if (!visualHarness.includes(pattern)) {
errors.push(`visual harness no longer intercepts daemon route ${pattern}`);
}
}
if (!visualHarness.includes("not mocked by visual coverage") || visualHarness.includes("route.continue()")) {
errors.push("visual harness no longer terminates unmocked daemon routes at its browser boundary");
}
const obsoleteWebWalker = path.join(
repoRoot,
"apps/web/tests/components/Theater/critique-coverage.test.ts",
);
try {
await access(obsoleteWebWalker);
errors.push("obsolete web-owned critique walker still consumes the daemon tree");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
// The authoritative cross-app walker lives in e2e/tests/critique-coverage.test.ts.
}
for (const consumer of await webDaemonFilesystemConsumers(repoRoot)) {
errors.push(`${consumer} reads the daemon tree from the web test lane`);
}
for (const needle of [
"needs.scopes.outputs.run_e2e_vitest == 'true'",
"needs.scopes.outputs.run_ui_p0 == 'true'",
"pnpm --filter @open-design/e2e test",
"include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}",
]) {
if (!ciWorkflow.includes(needle)) {
errors.push(`CI workflow no longer preserves daemon-core coverage: missing ${needle}`);
}
}
return errors;
}
export async function checkDaemonCoreBoundary(context: GuardContext): Promise<boolean> {
const errors = await daemonCoreRepositoryContractErrors(context.repoRoot);
if (errors.length > 0) {
console.error("Daemon core boundary violations found:");
for (const error of errors) console.error(`- ${error}`);
return false;
}
console.log("Daemon core boundary check passed: retained behavior lanes and skipped consumers stay isolated.");
return true;
}
-1057
View File
File diff suppressed because it is too large Load Diff
+83 -108
View File
@@ -1,7 +1,7 @@
# CI scope confidence methodology
This is the current authority for CI scope confidence rules in
`scripts/scopes.ts`, their guard requirements, and their evidence recipes.
`.github/config/scopes.json`, their planner invariants, and their evidence recipes.
Workflow topology and the capability/handoff architecture stay owned by
`.github/AGENTS.md`; do not restate them here.
@@ -14,7 +14,7 @@ that change history.
## The model in three paragraphs
Every changed file is classified by the additive rule table in
`scripts/scopes.ts`: effects union across matched rules, confidence is the
`.github/config/scopes.json`: effects union across matched rules, confidence is the
minimum across matched rules. Each evaluation context brings a trust threshold:
PR and manual-hot runs believe `medium`, the merge queue believes only
`certain`, manual-full runs believe nothing. Renames contribute both the
@@ -22,19 +22,50 @@ current and previous filename so moving a file cannot discard the source
path's validation effects. A file below threshold — or
matching no rule — escalates fail-closed to the full radius.
The policy floor never moves: `run_preflight` is true in every plan, and its
workspace setup, `pnpm guard`, and i18n structure check always execute. Broad
The scope policy floor never moves: `preflight` is enabled in every scope plan.
Its current `"*"` hash declaration makes workspace setup, `pnpm guard`, and
i18n structure checks execute for every new tracked tree, while an identical
cached invocation may skip the whole job. Broad
app declaration builds, workspace typecheck, and `run_workspace_unit_tests`
may skip only for a merge-queue plan whose certain-tier evaluation claims zero
validation effects. PR, manual-hot, forced-full, and escalated queue plans keep
all broad workspace validation.
`scripts/scopes.ts` remains an install-independent preinstall entrypoint.
`scripts/guard.ts` is the postinstall policy-floor entrypoint and composes its
shared mechanism and scope contracts from `scripts/lib/guard/`. The
`scripts library architecture` guard keeps those layers acyclic, prevents
scope startup from reaching guard or third-party dependencies, and keeps CLI
process control out of the library closure.
`.github/scripts/scopes.py` is the install-independent, Linux workflow-control entrypoint.
Its rule and matrix data lives in `.github/config/scopes.json`; it never imports
workspace code. `.github/scripts/runners.py` and `.github/scripts/hash.py` share
the same stdlib-only cold-start boundary.
`scripts/guard.ts` is a downstream repository-policy entrypoint. It runs only
after the plan exists and therefore does not authorize scope classification or
workload omission. The planner validates its own configuration and routing
contract before emitting any workload decision; repository guards remain
useful checks, but they are not part of the planner's trust chain.
## Orthogonal hash composition
The scope planner answers whether a workload is relevant to the changed-file
context. The hash register answers whether that workload's declared Git input
combination differs from the previous invocation on the branch. CI runs a
workload only when `scope_enabled && !hash_equal`; fine-grained commands inside
the workload remain a separate business-layer concern.
Declarations live in `.github/config/hash.json`. A declaration may contain Git
paths/globs, `suite://<name>` reusable path groups, `key://<workflow>/<identity>`
dependencies, or `"*"` for the entire tracked tree. Cycles, dangling references,
unsafe paths, empty matches, schema drift, and scope/hash identity drift fail at
the Linux plan entrypoint before workload dispatch. The initial contract uses
`"*"` for every identity; narrow closures require high-confidence evidence and
may be introduced independently later.
Actions cache stores only the previous identity-to-hash map. `hash.py` reads it,
computes and compares every current identity, then atomically replaces the local
state before workloads run. The plan transfers that pending map to `validate`,
which publishes it to Actions cache only after the gate succeeds. The map carries
no job-success, retry, or reliability meaning; success controls publication, not
payload. Restore, transfer, and save failures are non-fatal and therefore start
cold; invalid configuration is fatal. Only
`if: ${{ fromJSON(needs.plan.outputs.run).<identity> }}` in `ci.yml` turns the
static comparison into a skip.
The error cost is asymmetric by tier. A wrong `medium` rule under-arms a PR
run and gets caught by the merge queue's stricter threshold — cost: one queue
@@ -53,65 +84,37 @@ frequency-weighted tonnage lists barely intersect).
## Certain-tier requirements
A PR that makes a rule `certain` must be statable in three sentences: which
rule, what guard, how much tonnage. Anything that cannot fit that statement is
riding along and must be split out.
`certain` is an operational planner policy, not a proof that semantic
dependencies are complete. A downstream job, including `pnpm guard`, cannot
authorize an omission already made by the plan that scheduled it.
Requirements:
1. **A defensible core.** Promote the subset of the surface whose boundary
invariant is local and checkable. Split the rule if needed. Example: the
global `*.md` regex is permanently medium because its safety depends on
*other* rules covering every runtime-markdown directory — a cross-rule
invariant no local guard can keep.
2. **A guard that resolves.** The rule's `guard` field must name a live
`scripts/guard.ts` check (`pnpm --silent guard --list-checks` is the
registry; the rule-table invariant test enforces resolution). Guards for
certain rules must run in the policy floor — `pnpm guard` in preflight
qualifies — so the check that justifies skipping always itself runs.
3. **Evidence proportional to the guard's strength.** Guard invariants come in
three strengths: *definitional* (the surface cannot enter build or runtime
by construction — e.g. docs), *structural* (an import-graph boundary), and
*behavioral* (a topology test). Definitional rules may rely on replay
evidence alone. Structural and behavioral rules additionally require at
least 10 qualifying single-PR queue groups from the latest 400 first-parent
merges. Native `ifTrustAll` traces are preferred; paired evidence also
qualifies when the PR ran the candidate medium plan for the same file set,
the real queue group ran the full plan, both succeeded, and the proposed
plan has not weakened since that pair.
4. **Goldens updated, divergence pinned.** The golden that changes is the
proof of the behavior change; the goldens that do not change are the proof
of its containment.
5. **Exceptions bind to checkable preconditions.** Every guard allowlist entry
is a claim, and claims split by what justifies them. A *local, definitional*
fact ("this string is passed as data to a pure function, never opened") may
stay prose — it can only be falsified by editing the allowlisted file
itself, which puts the entry in front of a reviewer. A *remote, mutable*
fact ("that lane doesn't run this file", "that workflow is outside the
gate") must not be trusted as prose: the guard verifies the fact and drops
the exception the moment it stops holding, so the failure mode is a loud
guard report at the change that broke the premise — not a rationale that
rotted silently years earlier. Worked example: the consumption guard
tolerates `apps/daemon/tests/runtimes/trae-cli.test.ts` reading
`docs/agent-adapters.md` because that exact document is classified as
daemon core. Editing the consumed document therefore runs the same full
daemon suite as editing its consumer; the allowlist cannot create a skipped
producer/consumer edge.
1. **A conservative rule-table boundary.** Keep promoted matches explicit and
narrow. Unknown, mixed, empty-unresolved, invalid, or below-threshold inputs
must select the full plan.
2. **Planner-owned validation.** `python3 .github/scripts/scopes.py validate`
must reject schema drift, unknown effects, invalid regexes, match cycles,
malformed or duplicate matrices, and invalid UI P0 shadow references before
any workload decision is emitted.
3. **Direct planner behavior tests.** Goldens invoke `scopes.py plan` itself for
representative in-bound, out-of-bound, mixed, and fallback inputs. Do not
reimplement the evaluator in another language and compare two copies.
4. **Measured operational evidence.** Replay and paired-run evidence quantify
how often a rule applies and whether the retained plan has passed in
practice. This evidence can justify an operational decision, but it must not
be described as a complete dependency proof.
No general demotion policy is defined. One hard rule is active: if a guard
check is deleted or renamed, the rule-table invariant test fails CI — a
certain rule can never silently outlive its guard. Rule five is the same
principle one level down: an exception can never silently outlive its premise.
Independent semantic-closure guards may be evaluated later. They must sit
outside the planner's scheduling authority before their evidence can strengthen
a `certain` claim.
## Certain-exempt boundary
Rule `certain-exempt-surface`: prefixes `docs/`, `apps/landing-page/`,
`.vscode/`, `.idea/`, `.github/ISSUE_TEMPLATE/` plus exacts `LICENSE`,
`.github/CODEOWNERS`. Guard: `certain-exempt surface consumption`
(`scripts/check-certain-exempt-consumption.ts`) — no skippable-lane source may
reference a certain-exempt path; policy-floor code (root `scripts/`) is exempt
from the scan because preflight always runs and may validate docs content
(product neutrality does).
`.github/CODEOWNERS`. The planner owns this classification directly; no
downstream guard is treated as proof that these files are unconsumed.
Current evidence and exceptions:
@@ -120,10 +123,6 @@ Current evidence and exceptions:
- Root markdown such as `README.md` remains medium because bare filename
literals are widespread as project-fixture data and are not locally
distinguishable from repository-root reads.
- Allowlisted true consumer:
`tools/release/src/release-note/prepare.ts` reads `docs/CHANGELOG`, which
executes only in release workflows; `@open-design/tools-release` tests run
in no `ci.yml` lane.
## Certain packaged-leaf boundary
@@ -140,23 +139,10 @@ the focused packaged launcher update-loop fallback, and Windows launcher
payload tests. It skips web workspace tests, broad E2E Vitest, UI P0, critical
Playwright, and visual Playwright.
Guard: `packaged leaf boundary`
(`scripts/check-packaged-leaf-boundary.ts`). The policy-floor check scans
skippable-lane source for package imports and repository paths entering the
certain core, verifies that every core sample resolves to exactly the guarded
effects at the certain threshold, and pins the workspace-unit command block.
Allowed consumers are limited to:
- `tools/dev/`, whose tests stay armed by the certain rule;
- the focused packaged launcher update-loop test;
- scope, workflow, cross-app, fork-approval, and package-manager invocation
fixtures that treat the paths as data;
- the packaged and tools-pack esbuild entry configs, which own their source
entrypoints while config changes themselves remain medium-tier.
Package manifests, build configs, bins, vendor content, and files outside the
listed core remain medium. A mixed queue group containing any medium file
still escalates to the full plan.
still escalates to the full plan. Direct `scopes.py plan` tests pin the retained
effects and escalation behavior; they do not claim to prove every consumer.
Current evidence:
@@ -186,19 +172,10 @@ to exercise daemon buildability, user-level API/runtime behavior, and every
merge-gated UI P0 capability without treating web-owned rendering tests or
packaging-format tests as daemon consumers.
Guard: `daemon core boundary` (`scripts/lib/guard/scope.ts`). The policy-floor
check verifies that:
- representative source, markdown, and test files resolve only to the certain
daemon rule and its exact guarded effects;
- the daemon sidecar subtree, runtime-definition shadow, and daemon package
manifest still escalate;
- the workflow continues to execute E2E Vitest and the full UI P0 matrix;
- web code cannot import another app's private implementation, and web tests
do not read the daemon tree through filesystem APIs;
- the visual harness intercepts every daemon-owned route family; explicit
visual fixtures win and every remaining request terminates with a
deterministic browser-side 404.
Direct `scopes.py plan` tests pin representative daemon-core routing and
out-of-bound escalation. General cross-app and visual-harness guards remain
repository checks, but they do not authorize the planner's daemon-core
omissions.
The authoritative cross-app critique coverage walker lives in
`e2e/tests/critique-coverage.test.ts`, which remains armed by the daemon-core
@@ -206,7 +183,7 @@ plan. The latest 400 first-parent merges contain 78 pure daemon-core groups.
Fifteen recent groups have successful narrow PR validation paired with
successful full merge-group validation. A representative full queue run spends
about 20 runner-minutes in the web, visual, and Windows jobs omitted by the
guarded plan; UI P0 remains the critical path.
planner; UI P0 remains the critical path.
## Daemon UI P0 capability shadow
@@ -220,7 +197,7 @@ The `daemon-runtime-definition` capability matches changes confined to:
- `capabilities.ts`, `local-profiles.ts`, `metadata.ts`, and `registry.ts`
directly under `apps/daemon/src/runtimes/`;
- the explicit companion-test list in
`DAEMON_RUNTIME_DEFINITION_EXACT` (`scripts/scopes.ts`).
the `daemon-runtime-definition` exact list (`.github/config/scopes.json`).
Its candidate keeps `entry-settings`, `project-workspace`, and
`project-runtime`, and omits only `workspace-restoration`. The project
@@ -229,11 +206,9 @@ and model selector. Any empty, unresolved, mixed, unknown, or out-of-surface
change falls back to the full four-domain matrix and records the reason in
`trace.uiP0Shadow`.
Guard: `UI P0 shadow contract` (`scripts/lib/guard/scope.ts`). It pins the
applied full matrix, the candidate group set, representative in-bound
resolution, and full fallback for shared daemon, runtime-composition, web, and
unresolved inputs. The shadow must accumulate successful paired runs before it
can become an execution input under the certain-tier requirements.
Direct `scopes.py plan` tests pin the applied full matrix, candidate group set,
representative in-bound resolution, and full fallback. The shadow must
accumulate successful paired runs before it can become an execution input.
The latest-400 first-parent replay contains three matching groups. The
candidate would avoid one UI P0 worker per matching group, currently about
@@ -249,11 +224,9 @@ The predicate is queue-only: PR/manual-hot run broad validation even when the
medium-tier plan has no effects, and forced-full or escalated queue plans run
everything.
The certain-exempt consumption guard executes in preflight, and `pnpm guard`
sees every changed path (including a misleading executable such as
`docs/example.js`). The workspace-unit job does not own landing-page
validation, and the broad workspace typecheck excludes
`@open-design/landing-page`.
`pnpm guard` still runs as ordinary policy-floor work when preflight is
enabled, but its result does not authorize the zero-effect plan. The planner's
classification and fail-open behavior are the operative contract.
The 398-merge replay ending at `b99a9fdc3` contains 46 qualifying queue plans
(11.6%). A sample of 12 successful merge-group runs measures broad prebuild
@@ -265,7 +238,7 @@ window).
## Evidence recipes
Design rule: shell only fetches file lists and extracts logs; every scope
judgment goes through `scripts/scopes.ts plan`. Never reimplement rule
judgment goes through `.github/scripts/scopes.py plan`. Never reimplement rule
semantics in a pipeline.
Replay recent merges through the evaluator (candidate tonnage):
@@ -273,7 +246,7 @@ Replay recent merges through the evaluator (candidate tonnage):
```bash
git log --first-parent -400 --pretty=%H origin/main | while read -r sha; do
git diff-tree -r --name-only --no-commit-id "$sha^" "$sha" |
node --experimental-strip-types scripts/scopes.ts plan \
python3 .github/scripts/scopes.py plan \
--context merge-queue --files-from - |
node -e 'const d=JSON.parse(require("fs").readFileSync(0,"utf8"));
console.log(d.trace.escalations.length === 0 ? "PURE" : "ESCALATED")'
@@ -283,7 +256,7 @@ done | sort | uniq -c
Classify one change set offline (PR-side view, prints `{ plan, trace }`):
```bash
node --experimental-strip-types scripts/scopes.ts plan --context pr \
python3 .github/scripts/scopes.py plan --context pr \
--files apps/web/src/App.tsx docs/architecture.md
```
@@ -307,7 +280,9 @@ infrastructure.
## Open questions
- Demotion policy beyond the guard-resolution hard rule.
- A demotion policy for `certain` rules when planner evidence becomes stale.
- What independent evidence source could strengthen semantic closure without
being scheduled by the plan it is meant to assess.
- Whether medium-tier zero-effect PR plans should use the policy floor; this
needs its own evidence and containment review.
- Queue batching discount: the 11.6% figure assumes single-PR queue groups; a