software-defence-factory 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ instructions, existing source and session decisions. Preserve useful work and
10
10
  licenses. Assessment-only requests stay read-only. A method-only adoption needs
11
11
  no runtime, Docker host or model account.
12
12
 
13
- The [setup plan](../../docs/setup.md) owns supported commands and checkpoints.
13
+ The [setup plan](../../../docs/setup.md) owns supported commands and checkpoints.
14
14
  Read the relevant sections rather than inventing a host-specific setup recipe.
15
15
  Choose this skill explicitly when adopting Factory. Project preparation tools
16
16
  need no knowledge of Factory and do not invoke this transition automatically.
@@ -24,7 +24,7 @@ Use the actual host's OS, service manager, hardware and private network; do not
24
24
  assume a machine model, a VPN provider, a cloud, Git forge, issue tracker, CI
25
25
  provider or particular harness. Inspect the configured remote and actual provider
26
26
  capabilities. GitHub is one adapter; an unsupported host keeps local execution
27
- without guessed API calls. Use [integration ownership](../../docs/integrations.md).
27
+ without guessed API calls. Use [integration ownership](../../../docs/integrations.md).
28
28
 
29
29
  Use the existing task/issue record for missing obligations and evidence. Avoid a
30
30
  second project registry or a template conversion of an existing application.
@@ -38,9 +38,9 @@ Continue authorized repairs; ask only for decisions or authority actually missin
38
38
  repository instructions. Adapt the staged method to the
39
39
  project; do not overwrite its files. Reconcile the chosen provider’s issue forms/labels (where supported), CI triggers,
40
40
  required checks, protection rules and the intended PR/release path using
41
- [repository readiness](../../kit/repository.md). Before trusted GitHub PR
41
+ [repository readiness](../../../kit/repository.md). Before trusted GitHub PR
42
42
  delivery, confirm active candidate-triggered workflows fit the bounded
43
- qualification in [recovery](../../docs/recovery.md#trusted-pr-delivery);
43
+ qualification in [recovery](../../../docs/recovery.md#trusted-pr-delivery);
44
44
  unsupported workflows keep delivery patch-only. Organization hooks and
45
45
  external CI remain operator-owned. File presence is not proof.
46
46
  - **Infrastructure:** choose a private state directory, loopback port, host and
package/README.md CHANGED
@@ -39,9 +39,12 @@ The runtime supplies policy and six focused skills to its isolated jobs. `init`
39
39
 
40
40
  ## How the factory works
41
41
 
42
- [![Factory setup, execution and delivery](https://raw.githubusercontent.com/arcitai/software-and-defence-factory/main/docs/architecture.svg)](https://github.com/arcitai/software-and-defence-factory/blob/main/docs/architecture.excalidraw)
42
+ <a href="docs/architecture.md"><img src="docs/diagrams/deployment.svg" width="720" alt="Factory setup: operator access to a private execution host with Docker, a chosen harness and inference; application deployment remains in its own CI/CD."></a>
43
43
 
44
- [Architecture and boundaries](docs/architecture.md) · [Editable Excalidraw source](https://github.com/arcitai/software-and-defence-factory/blob/main/docs/architecture.excalidraw)
44
+ [Architecture and boundaries](docs/architecture.md). Editable views:
45
+ [Setup / deployment](docs/diagrams/deployment.excalidraw) ·
46
+ [Runtime layers / ownership](docs/diagrams/architecture.excalidraw) ·
47
+ [Work lifecycle](docs/diagrams/lifecycle.excalidraw)
45
48
 
46
49
  Each result belongs to a specific candidate commit and policy. A failed check blocks delivery. Changing the candidate or check policy invalidates earlier evidence. Approval records a handoff; publishing, merging and deployment follow the application's separate authority.
47
50
 
@@ -58,8 +61,8 @@ Start setup with `factory foundation` and the [Factory Foundation plan](docs/set
58
61
  | `bin/` | CLI entry point |
59
62
  | `factory/` | Queue, HTTP API, isolation, evidence, updates and bundled dashboard assets |
60
63
  | `dashboard/` | Dashboard source and UI tests |
61
- | `kit/`, `.agents/skills/` | Portable method, adoption records and six job skills |
62
- | `operator-skills/` | Factory Foundation setup guidance; never mounted into jobs |
64
+ | `kit/`, `kit/skills/` | Portable method, adoption records and the canonical six job skills |
65
+ | `.agents/skills/` | Repository/operator guidance, including explicit Factory Foundation adoption; outside the job catalog |
63
66
  | `scripts/`, `tests/` | Packaging, qualification, release checks and behavioral tests |
64
67
  | `docs/` | Setup, architecture, recovery, proof and ownership |
65
68
 
@@ -35,6 +35,7 @@ shell endpoint or a second scheduler.
35
35
  | Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
36
36
  | Immutable source admission | `init --source-ref`, `run --source-ref`, `issue start --source-ref`; status and build evidence carry the resolved SHA | `POST /api/v1/jobs` resolves/retains before acknowledgement; shared source metadata in status | Issue Start work, local request and revision forms accept a ref; task detail shows requested ref, resolved SHA and prior source commits | Build/retry use retained objects; revisions start fresh by default; explicit continuation keeps the reviewed tree and recorded source; a new ref replaces the base; legacy source remains unknown |
37
37
  | Trusted PR handoff | `publish JOB_ID` publishes/reconciles; `abandon-delivery JOB_ID --branch-sha SHA` records a checked local resolution for a pre-write branch collision | Authenticated `POST /api/v1/jobs/:id/publish` and `/abandon-delivery`; shared receipt, conflict identity and removal policy | Publish/reconcile and explicit “Abandon local delivery; keep remote branch” actions share controller state; errors/results and inspected branch identity are visible | New writes require matching protected Codex/Pi build/review provenance, deterministic verify/handoff provenance, non-synthetic bound artifacts and a qualified GitHub Actions tree. Shared delivery status exposes `workflow_qualification` and the same reason blocks CLI/API/dashboard capability and publication/retry. Candidate workflow changes, unsupported triggers/syntax, or active generated-push, selected-ref-dispatch and PR jobs with write/secrets/environment/OIDC/deploy access, self-hosted runners or ambiguous privileged guards refuse trusted writes. Supported ASCII guard comparisons follow GitHub's case-insensitive string semantics; unknown PR refs, non-ASCII mismatches, and glob/escaped branch filters cannot prove a privileged job inactive. The shared summary's `action_mode` distinguishes new/resumable publication from read-only reconciliation and drives idle and pending task button wording. Branch-only collisions and unknown/abandoned states offer neither; known PR receipts and pending PR-creation checkpoints retain read-only reconciliation. Abandonment checks the current run, saved intent, exact branch head and absence of an associated PR; it writes no provider data, preserves the remote branch/evidence, disables republishing and permits local removal. Uncertain effects and incompatible evidence stay blocked. Destination remains private operator config; patch-only remains default |
38
+ | Delivered-commit check readback | `publish JOB_ID` reconciles, `status` returns saved checks | Existing publish/status routes return the same `delivery_status.checks` | Delivered-commit checks, per-row conclusions/scope, uncertainty and refresh | 0.13.1 verifies exact commit/provider identity; installed/live/browser qualification pending; no required-check or mergeability claim |
38
39
  | Optional trusted web verification | `web probe` performs a real local Chromium interaction; `doctor` reports readiness | Verify stores a shared story summary and protected JSON artifact in the run | Task history shows passed/failed/unavailable/inconclusive plus tool, candidate, policy and story hashes | Disabled by default. Required operator stories and Playwright/Chromium image ID are frozen in attempt policy. Linux Chromium proof cannot qualify native/mobile OS behavior; see [the browser contract](web-verification.md) |
39
40
 
40
41
  The current generic task form can name the Defence workflow; that is not a
@@ -128,3 +129,50 @@ remain visible when it is closed. Rows and cards share compact metadata; source
128
129
  readiness remains distinct from the runtime badge, and full assignments and
129
130
  linked attempts remain in detail. These presentation controls do not change
130
131
  headless records or admission semantics.
132
+
133
+ ## Delivered-commit checks (0.13.1, bounded #37 slice)
134
+
135
+ The controller keeps its before/after PR repository, branch, target and immutable
136
+ head guards. The GitHub adapter reads that delivered head, not a moving ref or
137
+ merge commit. CLI/API/dashboard share the saved normalized `checks` object:
138
+
139
+ - `target`: requested `repository` URL, immutable `sha`, validated
140
+ `pull_request_number`; null for an invalid request. This names the read target,
141
+ not proof that every returned row belongs to it.
142
+ - Each check run retains `head_sha`, raw `conclusion`, `status`, `passed` and
143
+ `non_blocking`; `scope` is `commit`, `pull_request` or `unknown`. An exact
144
+ `head_sha` with `pull_requests: []` is commit-scoped. Missing/malformed or
145
+ conflicting associations do not qualify. Commit statuses use the combined
146
+ response's exact SHA and repository identity and carry the same scope/flags.
147
+ Supplied PR `head`/`base`, repository and owner containers must be objects,
148
+ never null, scalars or arrays. Supplied repository names, owner login and
149
+ API/HTML URLs must agree, including on combined statuses; supplied combined
150
+ commit/status URLs must identify the exact target. Optional association details
151
+ may be absent. A supplied head SHA must match; a supplied base SHA must be
152
+ well formed but may differ from the delivered head. Combined statuses still
153
+ require a repository `full_name` and exact commit SHA.
154
+ - Optional row API self `url` fields must name the requested repository and
155
+ `check-runs/{id}` or `statuses/{id}` resource, agreeing with a supplied numeric
156
+ row ID. The documented legacy `statuses/{sha}` form must name the exact delivered
157
+ SHA; its suffix is a commit identity, not a row ID. Missing self URLs remain
158
+ supported. Malformed or contradictory self URLs make that row's status/scope
159
+ unknown and both flags false. CI details/HTML/target links are presentation
160
+ links, not repository identity; external CI destinations remain usable.
161
+ - `pagination_complete` requires both arrays and exact nonnegative totals
162
+ within the existing 100-row bound. No extra pages or polling are introduced.
163
+ Empty, partial, unreadable or unknown data never establishes aggregate success;
164
+ `reason` explains unknown results. Verified failures remain visible with partial
165
+ data. Otherwise incomplete/unverified data takes precedence over cancellation,
166
+ pending and positive results.
167
+ - `success` requires at least one executed success and all observed rows to be
168
+ non-blocking. Skipped/neutral-only results are `non_blocking`, not passing
169
+ execution. `pending`, `failure`, `cancelled` and `unknown` remain distinct;
170
+ raw conclusions retain timeout/action-required and unrecognized outcomes.
171
+
172
+ Old receipts are not rewritten. Without matching target/scope metadata the
173
+ human view marks recorded results unverified and offers the existing refresh;
174
+ headless consumers must likewise treat absent provenance as unknown. These are
175
+ observations of delivered-commit checks, not proof of required-check completion,
176
+ branch protection, mergeability, deployment or model quality. Protected Verify,
177
+ independent Review and explicit acceptance remain unchanged. #37 remains open
178
+ for the other parity gaps above; no role/profile or workflow redesign is included.
package/docs/npm.md CHANGED
@@ -104,8 +104,8 @@ installation or retained attempt needs them.
104
104
 
105
105
  ## Protected evidence compatibility
106
106
 
107
- Factory 0.12.0 recognizes version-1 execution profiles emitted by native
108
- **0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.11.1, 0.11.2 and 0.12.0**. This is an exact allowlist in
107
+ Factory 0.13.1 recognizes version-1 execution profiles emitted by native
108
+ **0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.11.1, 0.11.2, 0.12.0, 0.13.0 and 0.13.1**. This is an exact allowlist in
109
109
  `factory/execution-profile.mjs`, independent of the installed package version;
110
110
  it is not a semver range or an automatic promise for later releases. Unknown
111
111
  runtime strings, unknown profile formats and incomplete legacy acceptance
@@ -153,6 +153,28 @@ Version 0.12.0 adds the all-work Inbox read model and restores the retained
153
153
  overview. Protected execution writers and acceptance/publication guards are
154
154
  unchanged; the explicit compatible-writer entry preserves all checks above.
155
155
 
156
+ Version 0.13.0 relocates byte-identical runtime skill instructions to
157
+ `kit/skills/` and adds a read-only alias of that catalog for Codex discovery.
158
+ The audit covers `execution-profile.mjs`, `processes.mjs`, executor candidate,
159
+ check, review and acceptance writers, `execution-evidence.mjs` and both delivery
160
+ capability/validation paths. The v1 schema, candidate reconstruction, current
161
+ policy checks, isolation and model credential rules are unchanged. Foundation
162
+ is outside the job catalog. Supported old immutable evidence remains subject
163
+ to every existing guard; unknown later versions receive no automatic trust.
164
+
165
+ Version 0.13.1 changes only delivered-commit check readback and its presentation.
166
+ The same writer/validator audit above found no changes to protected v1 execution,
167
+ candidate, check, review or acceptance writers, policy binding, isolation or
168
+ credentials. The explicit 0.13.1 entry is covered by retained-profile capability
169
+ and publication validation tests; old records remain immutable and unknown
170
+ versions remain blocked. Provider check observations are separate from protected
171
+ Verify evidence and cannot authorize acceptance or publication.
172
+
173
+ Old installed releases retain their own files and mount paths until an idle,
174
+ reviewed update. No installed catalog, execution profile or historical hash is
175
+ rewritten by this layout change. Qualify the exact updated package/image before
176
+ resuming work; retained releases/evidence stay available for recovery.
177
+
156
178
  New publication still requires the current remote target to equal the original
157
179
  accepted base; reviewed-candidate continuation preserves that baseline, while
158
180
  target refresh remains #72. See [the verification map](https://github.com/arcitai/software-and-defence-factory/blob/main/docs/proof.md) for delivered
package/docs/setup.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # Factory Foundation: repository and execution setup
2
2
 
3
- The operator skill is available through `factory foundation`.
3
+ The operator skill is available through `factory foundation`, including from
4
+ an installed npm package without a source checkout. Its canonical source is
5
+ [.agents/skills/factory-foundation/SKILL.md](../.agents/skills/factory-foundation/SKILL.md).
6
+ Use it explicitly for adoption; it is independent of AIOS. Repository/operator
7
+ guidance stays outside the runtime catalog at `kit/skills/`.
4
8
  See [Factory concepts](concepts.md) for host, worker, harness and agent roles.
9
+ The [setup/deployment view](architecture.md#setup-and-deployment) shows operator
10
+ access, the private execution host and the separate application delivery boundary.
5
11
 
6
12
  Use this plan for a new installation or when moving an existing Factory to an
7
13
  execution host. Complete the applicable checkpoints in order and record the
@@ -36,7 +42,19 @@ this plan.
36
42
 
37
43
  For method-only adoption, run `factory kit --output NEW_DIRECTORY`
38
44
  and follow [the adoption guide](../kit/README.md). The remaining host/runtime
39
- steps apply only when using Factory's optional controller.
45
+ steps apply only when using Factory's optional controller. Export maps the six
46
+ job skills into staged `.agents/skills/`; it never changes the application's
47
+ AGENTS.md or installs global skills. Foundation stays in the installed package.
48
+
49
+ Runtime jobs receive only the reviewed catalog as read-only `/factory-skills`.
50
+ Codex agent phases also discover that same catalog at `/etc/codex/skills`; Pi
51
+ uses `--skill /factory-skills`. Verify discovery on the exact installed image
52
+ without credentials or inference (Codex native listing and Pi RPC
53
+ `get_commands`), then qualify inference separately when authorized. Custom
54
+ harness discovery remains operator-owned. A directory listing alone is not
55
+ native discovery proof. Bundled instructions install no model, browser, Docker
56
+ or provider access. Repository guidance remains readable project context and
57
+ cannot expand job permissions.
40
58
 
41
59
  ## 2. Establish host access and boot prerequisites
42
60
 
package/docs/workflows.md CHANGED
@@ -15,12 +15,16 @@ Defence currently runs **Investigate** against supplied scoped evidence and
15
15
  produces a private draft. It is not production monitoring, exploitation or
16
16
  verified recovery. See [Defence integration](defence-integration.md).
17
17
 
18
- The six bundled job skills cover triage, specification, implementation, review,
18
+ The six bundled job skills in `kit/skills/` cover triage, specification, implementation, review,
19
19
  security and evaluation. They are instructions, not six running processes.
20
20
  All are mounted read-only for agent steps; the role prompt supplies the work
21
21
  boundary. Triage/specification prepare scope before admission; evaluation is a
22
22
  separately scoped comparison. Factory Foundation is an operator setup skill,
23
- kept outside those execution mounts.
23
+ at `.agents/skills/factory-foundation/SKILL.md`, used explicitly for adoption
24
+ and kept outside those execution mounts. CLI/API/Skills show actual installed
25
+ paths, content and SHA-256. Codex additionally mounts the same runtime catalog at
26
+ `/etc/codex/skills`; Pi uses `--skill /factory-skills`. Discovery on a particular
27
+ image needs native qualification, and skills provide neither tools nor access.
24
28
 
25
29
  ## Start work
26
30
 
@@ -24,7 +24,7 @@ const skillRoles = {
24
24
  export function factoryDefinition(config) {
25
25
  const harness = harnessOf(config);
26
26
  const skills = Object.entries(skillRoles).map(([role, purpose]) => {
27
- const id = `factory-${role}`, path = `.agents/skills/${id}/SKILL.md`;
27
+ const id = `factory-${role}`, path = `kit/skills/${id}/SKILL.md`;
28
28
  const content = readFileSync(join(ROOT, path), 'utf8');
29
29
  return { id, purpose, path, content, sha256: digest(content) };
30
30
  });
@@ -58,7 +58,7 @@ export function factoryDefinition(config) {
58
58
  }
59
59
 
60
60
  export function foundationSkill() {
61
- const path = 'operator-skills/factory-foundation/SKILL.md';
61
+ const path = '.agents/skills/factory-foundation/SKILL.md';
62
62
  const content = readFileSync(join(ROOT, path), 'utf8');
63
63
  return { id: 'factory-foundation', purpose: 'Prepare a repository and host for bounded Factory work', path, content, sha256: digest(content) };
64
64
  }
@@ -10,12 +10,15 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
10
10
  // 0.9.1, 0.10.0, 0.11.0 and 0.11.1 (unchanged protected evidence writers),
11
11
  // 0.11.2 (exact patch bytes and pre-acceptance reconstruction; same v1 schema).
12
12
  // 0.12.0 restores the Inbox read model/presentation; protected writers and guards are unchanged.
13
+ // 0.13.0 relocates identical job skills and adds a read-only Codex discovery alias.
14
+ // 0.13.1 corrects read-only delivered-commit checks; protected v1 writers are unchanged.
15
+ // Candidate/check/review/acceptance schemas, isolation and credential/permission guards are unchanged.
13
16
  // Older writers still require exact patch/tree reconstruction before publication.
14
17
  // Deliberately independent of VERSION: a release bump is not
15
18
  // evidence compatibility. Re-audit this list for every trust-relevant writer,
16
19
  // isolation or validation change; remove versions whose guarantees no longer
17
20
  // satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
18
- const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0']);
21
+ const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0', '0.13.0', '0.13.1']);
19
22
 
20
23
  export function isSupportedExecutionProfile(profile) {
21
24
  return profile?.version === 1 && SUPPORTED_EXECUTION_RUNTIMES_V1.has(profile.runtimeVersion);
@@ -91,7 +91,10 @@ async function container(mode, input, command, options = {}) {
91
91
  '--mount',`type=bind,source=${join(workspace,'.git')},target=/workspace/.git,readonly`];
92
92
  args.push('--mount',`type=bind,source=${reportDir},target=/output`,
93
93
  '--mount',`type=bind,source=${join(ROOT,'kit')},target=/factory-policy,readonly`,
94
- '--mount',`type=bind,source=${join(ROOT,'.agents/skills')},target=/factory-skills,readonly`);
94
+ '--mount',`type=bind,source=${join(ROOT,'kit/skills')},target=/factory-skills,readonly`);
95
+ // The same reviewed catalog supplies Codex native discovery; no operator skills.
96
+ if (mode !== 'verify' && harnessOf(config) === 'codex')
97
+ args.push('--mount',`type=bind,source=${join(ROOT,'kit/skills')},target=/etc/codex/skills,readonly`);
95
98
  if (scratch) args.push('--mount',`type=bind,source=${scratch},target=/scratch`);
96
99
  if (mode === 'verify') args.push('--env',`FACTORY_BASE_REVISION=${git('rev-parse',`${metadata().base}^{commit}`)}`);
97
100
  const logPath = join(folder, attempt, `${mode}.log`);
@@ -32,45 +32,92 @@ function slug(repository) {
32
32
  return `${match[1]}/${match[2]}`;
33
33
  }
34
34
 
35
- function checkSummary(runs, status, pullRequestNumber) {
36
- const checkRuns = Array.isArray(runs?.check_runs) ? runs.check_runs.slice(0, 100).map(run => ({
37
- kind: 'check_run',
38
- name: String(run.name || 'Unnamed check').slice(0, 200),
39
- status: pullRequestNumber && Array.isArray(run.pull_requests)
40
- && run.pull_requests.some(pull => pull.number === pullRequestNumber)
41
- ? ['queued', 'in_progress', 'completed'].includes(run.status) ? run.status : 'unknown'
42
- : 'unknown',
43
- conclusion: typeof run.conclusion === 'string' ? run.conclusion : null,
44
- url: typeof run.html_url === 'string' ? run.html_url : null,
45
- passed: run.status === 'completed' && run.conclusion === 'success'
46
- && pullRequestNumber && Array.isArray(run.pull_requests) && run.pull_requests.some(pull => pull.number === pullRequestNumber),
47
- non_blocking: run.status === 'completed' && ['success', 'skipped', 'neutral'].includes(run.conclusion)
48
- && pullRequestNumber && Array.isArray(run.pull_requests) && run.pull_requests.some(pull => pull.number === pullRequestNumber),
49
- })) : [];
50
- const contexts = Array.isArray(status?.statuses) ? status.statuses.slice(0, 100).map(item => ({
51
- name: String(item.context || 'Unnamed status').slice(0, 200),
52
- status: ['pending', 'success', 'failure', 'error'].includes(item.state) ? item.state : 'unknown',
53
- url: typeof item.target_url === 'string' ? item.target_url : null,
54
- })) : [];
55
- const rows = [
56
- ...checkRuns.map(item => ({ ...item, kind: 'check_run',
57
- passed: item.status === 'completed' && item.conclusion === 'success',
58
- non_blocking: item.status === 'completed' && ['success', 'skipped', 'neutral'].includes(item.conclusion) })),
59
- ...contexts.map(item => ({ ...item, kind: 'commit_status', passed: item.status === 'success', non_blocking: item.status === 'success' })),
60
- ];
35
+ // This is observed commit evidence, never branch-protection or acceptance policy.
36
+ function checkSummary(runs, status, target) {
37
+ const repositoryName = slug(target.repository).toLowerCase();
38
+ const repositoryAPI = `https://api.github.com/repos/${repositoryName}`;
39
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
40
+ const optionalIdentity = (value, expected) => value === undefined
41
+ || (typeof value === 'string' && value.toLowerCase() === expected);
42
+ // API self URLs identify a repository resource; CI details/target links do
43
+ // not. Status examples also use the legacy statuses/{sha} endpoint, whose
44
+ // suffix identifies the commit rather than the individual status ID.
45
+ const matchesSelfURL = (row, endpoint) => {
46
+ if (row.url === undefined) return true;
47
+ if (typeof row.url !== 'string') return false;
48
+ if (endpoint === 'statuses' && optionalIdentity(row.url, `${repositoryAPI}/statuses/${target.sha}`)) return true;
49
+ const match = row.url.match(/\/([1-9][0-9]*)$/);
50
+ return !!match && Number.isSafeInteger(Number(match[1]))
51
+ && optionalIdentity(row.url, `${repositoryAPI}/${endpoint}/${match[1]}`)
52
+ && (row.id === undefined || (Number.isSafeInteger(row.id) && String(row.id) === match[1]));
53
+ };
54
+ // Optional details may be absent, but supplied containers and identities must
55
+ // all agree. Optional chaining alone would erase malformed head/base values.
56
+ const matchesRepository = repo => repo === undefined || (object(repo)
57
+ && optionalIdentity(repo.full_name, repositoryName)
58
+ && optionalIdentity(repo.url, repositoryAPI)
59
+ && optionalIdentity(repo.html_url, target.repository.toLowerCase())
60
+ && optionalIdentity(repo.name, repositoryName.split('/')[1])
61
+ && (repo.owner === undefined || (object(repo.owner)
62
+ && optionalIdentity(repo.owner.login, repositoryName.split('/')[0]))));
63
+ const matchesPullSide = (side, head) => side === undefined || (object(side)
64
+ && (side.sha === undefined || (typeof side.sha === 'string' && /^[a-f0-9]{40}$/.test(side.sha)
65
+ && (!head || side.sha === target.sha)))
66
+ && matchesRepository(side.repo));
67
+ const checkRuns = Array.isArray(runs?.check_runs) ? runs.check_runs.slice(0, 100).map(run => {
68
+ const associations = run?.pull_requests;
69
+ const identity = object(run) && run.head_sha === target.sha && matchesSelfURL(run, 'check-runs') && Array.isArray(associations)
70
+ && associations.every(pull => object(pull) && pull.number === target.pull_request_number
71
+ && optionalIdentity(pull.url, `${repositoryAPI}/pulls/${target.pull_request_number}`)
72
+ && matchesPullSide(pull.head, true) && matchesPullSide(pull.base, false));
73
+ const runStatus = identity && ['queued', 'in_progress', 'completed'].includes(run?.status) ? run.status : 'unknown';
74
+ return {
75
+ kind: 'check_run',
76
+ name: String(run?.name || 'Unnamed check').slice(0, 200),
77
+ head_sha: typeof run?.head_sha === 'string' ? run.head_sha : null,
78
+ scope: identity ? (associations.length ? 'pull_request' : 'commit') : 'unknown',
79
+ status: runStatus,
80
+ conclusion: typeof run?.conclusion === 'string' ? run.conclusion : null,
81
+ url: typeof run?.html_url === 'string' ? run.html_url : null,
82
+ passed: runStatus === 'completed' && run.conclusion === 'success',
83
+ non_blocking: runStatus === 'completed' && ['success', 'skipped', 'neutral'].includes(run.conclusion),
84
+ };
85
+ }) : [];
86
+ const statusIdentity = object(status) && status.sha === target.sha
87
+ && object(status.repository) && typeof status.repository.full_name === 'string'
88
+ && matchesRepository(status.repository)
89
+ && optionalIdentity(status.commit_url, `${repositoryAPI}/commits/${target.sha}`)
90
+ && optionalIdentity(status.url, `${repositoryAPI}/commits/${target.sha}/status`);
91
+ const contexts = Array.isArray(status?.statuses) ? status.statuses.slice(0, 100).map(item => {
92
+ const identity = statusIdentity && object(item) && matchesSelfURL(item, 'statuses');
93
+ const itemStatus = identity && ['pending', 'success', 'failure', 'error'].includes(item?.state) ? item.state : 'unknown';
94
+ return {
95
+ kind: 'commit_status',
96
+ name: String(item?.context || 'Unnamed status').slice(0, 200),
97
+ head_sha: typeof status?.sha === 'string' ? status.sha : null,
98
+ scope: identity ? 'commit' : 'unknown',
99
+ status: itemStatus,
100
+ url: typeof item?.target_url === 'string' ? item.target_url : null,
101
+ passed: itemStatus === 'success', non_blocking: itemStatus === 'success',
102
+ };
103
+ }) : [];
104
+ const rows = [...checkRuns, ...contexts];
61
105
  const failed = rows.some(item => item.kind === 'check_run'
62
- ? item.status === 'completed' && ['failure', 'action_required', 'timed_out', 'cancelled'].includes(item.conclusion)
106
+ ? item.status === 'completed' && ['failure', 'action_required', 'timed_out'].includes(item.conclusion)
63
107
  : item.status === 'failure' || item.status === 'error');
108
+ const cancelled = rows.some(item => item.status === 'completed' && item.conclusion === 'cancelled');
64
109
  const pending = rows.some(item => item.status === 'pending' || item.status === 'queued' || item.status === 'in_progress');
65
- const unknown = rows.some(item => item.status === 'unknown'
110
+ const unknown = !statusIdentity || rows.some(item => item.status === 'unknown'
66
111
  || (item.kind === 'check_run' && item.status === 'completed'
67
112
  && !['success', 'skipped', 'neutral', 'failure', 'action_required', 'timed_out', 'cancelled'].includes(item.conclusion)));
68
- const complete = Number.isSafeInteger(runs?.total_count) && runs.total_count === checkRuns.length
69
- && Number.isSafeInteger(status?.total_count) && status.total_count === contexts.length;
113
+ const completePage = (payload, key) => Array.isArray(payload?.[key]) && payload[key].length <= 100
114
+ && Number.isSafeInteger(payload.total_count) && payload.total_count === payload[key].length;
115
+ const complete = completePage(runs, 'check_runs') && completePage(status, 'statuses');
70
116
  const onlyNonBlocking = rows.length > 0 && rows.every(item => item.non_blocking);
71
- const state = failed ? 'failure' : !complete || unknown ? 'unknown' : pending ? 'pending'
117
+ const state = failed ? 'failure' : !complete || unknown ? 'unknown' : cancelled ? 'cancelled' : pending ? 'pending'
72
118
  : onlyNonBlocking ? (rows.some(item => item.passed) ? 'success' : 'non_blocking') : 'unknown';
73
- return { state, pagination_complete: complete, check_runs: checkRuns, commit_statuses: contexts };
119
+ return { state, target, pagination_complete: complete, check_runs: checkRuns, commit_statuses: contexts,
120
+ ...(state === 'unknown' ? { reason: 'Check readback is empty, incomplete or contains unverified identity or status.' } : {}) };
74
121
  }
75
122
 
76
123
  export function githubDeliveryProvider({ request = requestGitHub } = {}) {
@@ -145,16 +192,17 @@ export function githubDeliveryProvider({ request = requestGitHub } = {}) {
145
192
  return get(`${apiRoot}/${slug(repository)}/pulls/${number}`);
146
193
  },
147
194
  async readChecks(repository, sha, pullRequestNumber) {
148
- if (!Number.isSafeInteger(pullRequestNumber) || pullRequestNumber < 1)
149
- return { state: 'unknown', check_runs: [], commit_statuses: [], reason: 'A pull request identity is required to associate checks with delivery.' };
195
+ if (!Number.isSafeInteger(pullRequestNumber) || pullRequestNumber < 1 || !/^[a-f0-9]{40}$/.test(sha || ''))
196
+ return { state: 'unknown', target: null, pagination_complete: false, check_runs: [], commit_statuses: [], reason: 'A valid pull request number and immutable delivered commit are required.' };
197
+ const target = { repository, sha, pull_request_number: pullRequestNumber };
150
198
  try {
151
199
  const [runs, status] = await Promise.all([
152
200
  get(`${apiRoot}/${slug(repository)}/commits/${sha}/check-runs?per_page=100`),
153
201
  get(`${apiRoot}/${slug(repository)}/commits/${sha}/status?per_page=100`),
154
202
  ]);
155
- return checkSummary(runs, status, pullRequestNumber);
203
+ return checkSummary(runs, status, target);
156
204
  } catch {
157
- return { state: 'unknown', check_runs: [], commit_statuses: [], reason: 'GitHub checks could not be read with the configured identity.' };
205
+ return { state: 'unknown', target, pagination_complete: false, check_runs: [], commit_statuses: [], reason: 'GitHub checks could not be read with the configured identity.' };
158
206
  }
159
207
  },
160
208
  };