mirror of
https://github.com/mvschwarz/openrig-registry.git
synced 2026-11-07 05:56:20 -10:00
Move the reviewed rig catalog and status tooling into this repository (#1)
* Move reviewed registry and status tooling from project world * Exercise submission validation during catalog migration * Remove the migration submission after its pull request checks pass --------- Co-authored-by: OpenRig contributors <[email protected]>
This commit is contained in:
1 parent
1865890a68
commit
33d4d44ca4
44 files changed
+17474
-2
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# Checks registry entries, behaviour views, journeys and the generated status file. It parses files in this
|
||||
# repository only: nothing a submission contains is fetched, installed or run, and the job holds a read-only token.
|
||||
name: Registry check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tools
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm ci --ignore-scripts --no-audit --no-fund
|
||||
- run: node --test test/*.test.mjs
|
||||
- run: node registry-check.mjs
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,190 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2026 Mike Schwarz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,5 +1,51 @@
|
||||
# OpenRig rig catalog
|
||||
|
||||
Reviewed rig listings and status for [openrig.dev/rigs](https://openrig.dev/rigs).
|
||||
Reviewed rig listings, submission requests, before-install diagrams and public status for
|
||||
[openrig.dev/rigs](https://openrig.dev/rigs).
|
||||
|
||||
Catalog migration arrives through a pull request. Bundles remain in their authors' repositories.
|
||||
The bundles stay in their authors' repositories. This repository records reviewed commits and generated views;
|
||||
it does not host or launch the bundles. OpenRig's workshop and factory-rsi sources stay in
|
||||
[openrig-world/rigs](https://github.com/mvschwarz/openrig-world/tree/main/rigs).
|
||||
|
||||
- [Submit a rig](registry/submissions/README.md): add one YAML file with its repository, folder and ref.
|
||||
- [Registry entries](registry/README.md): reviewed pins, configurations, digests and diagrams.
|
||||
- [Checks and status generation](tools/README.md): local validation and evidence-based status.
|
||||
|
||||
A listing means its pinned content was reviewed for listing. It is not a security audit and does not mean
|
||||
OpenRig ran or tested the team. The status data says what was tested separately.
|
||||
|
||||
## Workshop compatibility with OpenRig 0.6.6
|
||||
|
||||
This repository is the canonical registry. OpenRig 0.6.6's operator still reads the raw
|
||||
`openrig-world/registry/workshop.yaml` URL. That one file remains as a compatibility mirror; it is not a second
|
||||
catalog. New submissions, entries, diagrams and status changes belong here.
|
||||
|
||||
When changing the workshop pin, prepare paired pull requests here and in openrig-world. Copy the complete
|
||||
canonical entry to the old location, review both, and merge both before announcing the new pin:
|
||||
|
||||
```sh
|
||||
cp registry/workshop.yaml ../openrig-world/registry/workshop.yaml
|
||||
cmp registry/workshop.yaml ../openrig-world/registry/workshop.yaml
|
||||
```
|
||||
|
||||
Keep `source.repository` and `source.folder` pointing at the bundle in openrig-world. The mirror supplies the
|
||||
source commit and configurations to older operators; diagram files are served from this registry. Preserve the
|
||||
mirror while 0.6.6 installs still use it. A pin may require the existing two-step sequence: merge bundle source
|
||||
first, then review the registry entry for that resulting commit. These steps make no automated cross-repository
|
||||
writes.
|
||||
|
||||
## Check a change
|
||||
|
||||
```sh
|
||||
cd tools
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
npm test
|
||||
npm run check
|
||||
```
|
||||
|
||||
CI runs these checks on pull requests and pushes to main. It reads registry files; it does not execute submitted
|
||||
bundles. Status is generated from run records; it is not edited by hand.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](LICENSE), retained from openrig-world.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Registry
|
||||
|
||||
Each file here lists one rig bundle on openrig.dev/rigs. An entry records exactly what a maintainer reviewed:
|
||||
|
||||
- the source repository and folder, at a full 40-character commit (and the branch or tag it was submitted as);
|
||||
- each offered configuration, with its package digest, the OpenRig version that built it, and its generated
|
||||
behaviour view (under `views/`);
|
||||
- the review date, and whether it's `listed` or `withdrawn`.
|
||||
|
||||
Format: `registry-entry.v1` in the OpenRig repository's `docs/reference/bundle-formats.md`.
|
||||
|
||||
The site shows only `listed` entries, at their recorded commit. A later push to the bundle's branch changes nothing
|
||||
here until an update is reviewed and merged. A withdrawn entry leaves the lists.
|
||||
|
||||
Every listing carries the line: "Reviewed for listing on <date> at commit <short>. Review is not a security audit."
|
||||
|
||||
The registry check (`tools/registry-check.mjs`) runs on every pull request. It rejects entries that don't match the
|
||||
format, commits that aren't 40 hex characters, configuration IDs that aren't canonical, behaviour views that are
|
||||
missing or describe another package, and a status file that was edited by hand.
|
||||
|
||||
## Submitting a rig
|
||||
|
||||
Add `registry/submissions/<your-team>.yaml` with three fields (see `submissions/README.md`): your repository, the
|
||||
folder holding `rig.yaml`, and a branch, tag or commit. The check accepts it and says "Submission received". A
|
||||
maintainer then writes the full entry here, at a pinned commit, and removes the submission. A submission is never
|
||||
listed: the site and the status file read only the entries in this folder.
|
||||
@@ -0,0 +1,20 @@
|
||||
schema: openrig.registry-entry/v1
|
||||
slug: factory-rsi
|
||||
name: factory-rsi reference
|
||||
summary: "Retained reference: a seven-seat recursive improvement factory with a separate dogfood loop and human publication gate. No new native launch result is claimed."
|
||||
source:
|
||||
repository: https://github.com/mvschwarz/openrig-world
|
||||
folder: rigs/launch/factory-rsi
|
||||
resolvedCommit: f1d1d8fa4beb3f4f4a2fd13b1cb1b3cbda21b560
|
||||
requestedRef: f1d1d8fa4beb3f4f4a2fd13b1cb1b3cbda21b560
|
||||
configurations:
|
||||
- id: build.implementer=claude-code,check.qa=codex,dogfood.tester=codex,orch.lead=claude-code,plan.planner=claude-code,release.manager=claude-code,review.reviewer=codex
|
||||
alias: recommended
|
||||
recommended: true
|
||||
packageDigest: { algorithm: sha256, value: 206dfc1795d2ddfcdb77cc743d48084083adb6d6dfa1e4016cad2d5b9b76e68b, coverage: openrig.package-digest/v1 }
|
||||
assembler: { openrigVersion: 0.6.6 }
|
||||
behaviour: views/factory-rsi/build.implementer-claude-code_check.qa-codex_dogfood.tester-codex_orch.lead-claude-code_plan.planner-claude-code_release.manager-claude-code_review.reviewer-codex.0.6.6.json
|
||||
review:
|
||||
date: 2026-10-06
|
||||
note: Reviewed for listing. Validation and plan checks are not native runtime proof or a security audit.
|
||||
status: listed
|
||||
@@ -0,0 +1,17 @@
|
||||
# Submissions
|
||||
|
||||
To submit a rig bundle for openrig.dev/rigs, open a pull request that adds one file here, `<your-team>.yaml`, with
|
||||
exactly these three fields:
|
||||
|
||||
```yaml
|
||||
repository: https://github.com/<owner>/<repo>
|
||||
folder: rigs/my-team # the folder holding rig.yaml; "." for the repository root
|
||||
ref: main # a branch, tag or commit
|
||||
```
|
||||
|
||||
The registry check accepts it and says "Submission received". A maintainer then pins your link to an exact commit,
|
||||
reviews the bundle, writes the full entry in `registry/`, and removes this file. If it isn't listed, you get a short
|
||||
reply saying why.
|
||||
|
||||
Rig names on openrig.dev/rigs are unique. If `registry/<your-team>.yaml` already exists, the check still passes but
|
||||
says the name is taken; rename your file to a distinct one, for example `<taken-name>-<your-name>.yaml`.
|
||||
+2301
File diff suppressed because it is too large.
Load diff
+2865
File diff suppressed because it is too large.
Load diff
+2809
File diff suppressed because it is too large.
Load diff
+2753
File diff suppressed because it is too large.
Load diff
+2565
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
||||
schema: openrig.registry-entry/v1
|
||||
slug: workshop
|
||||
name: Workshop
|
||||
summary: "One four-seat software team: a Codex lead and reviewer, Claude Code
|
||||
builder and QA by default. The installing agent selects available runtimes
|
||||
while keeping the workshop name and one install directory."
|
||||
source:
|
||||
repository: https://github.com/mvschwarz/openrig-world
|
||||
folder: rigs/workshop
|
||||
resolvedCommit: 8b22d918340504ec2e01889a00b07c71195c459f
|
||||
requestedRef: 8b22d918340504ec2e01889a00b07c71195c459f
|
||||
configurations:
|
||||
- id: dev.build=claude-code,dev.qa=claude-code,dev.review=codex,orch.lead=codex
|
||||
alias: recommended
|
||||
recommended: true
|
||||
packageDigest:
|
||||
algorithm: sha256
|
||||
value: e2ea21e9867cb353fb06c08596c336ca08a9281c3fc231f9439d3d09cc7cc13d
|
||||
coverage: openrig.package-digest/v1
|
||||
assembler:
|
||||
openrigVersion: 0.6.6
|
||||
behaviour: views/workshop/dev.build-claude-code_dev.qa-claude-code_dev.review-codex_orch.lead-codex.0.6.6.json
|
||||
- id: dev.build=claude-code,dev.qa=claude-code,dev.review=claude-code,orch.lead=claude-code
|
||||
alias: all-claude
|
||||
packageDigest:
|
||||
algorithm: sha256
|
||||
value: 6bdb19e2e35e6da9808dc6c232b6fb10c28ba16b2a09dc2a5eb07c6cbfff5ad4
|
||||
coverage: openrig.package-digest/v1
|
||||
assembler:
|
||||
openrigVersion: 0.6.6
|
||||
behaviour: views/workshop/dev.build-claude-code_dev.qa-claude-code_dev.review-claude-code_orch.lead-claude-code.0.6.6.json
|
||||
- id: dev.build=codex,dev.qa=codex,dev.review=codex,orch.lead=codex
|
||||
alias: all-codex
|
||||
packageDigest:
|
||||
algorithm: sha256
|
||||
value: aed47f1207c528b5aa4f95532d7aad0a7564178aefc3bb0db7343cf07457a502
|
||||
coverage: openrig.package-digest/v1
|
||||
assembler:
|
||||
openrigVersion: 0.6.6
|
||||
behaviour: views/workshop/dev.build-codex_dev.qa-codex_dev.review-codex_orch.lead-codex.0.6.6.json
|
||||
- id: dev.build=pi,dev.qa=pi,dev.review=pi,orch.lead=pi
|
||||
alias: all-pi
|
||||
packageDigest:
|
||||
algorithm: sha256
|
||||
value: a8658fd3e06b3df76730b42ed84ddaf28677829815da1f370c76b1c885dc44e5
|
||||
coverage: openrig.package-digest/v1
|
||||
assembler:
|
||||
openrigVersion: 0.6.6
|
||||
behaviour: views/workshop/dev.build-pi_dev.qa-pi_dev.review-pi_orch.lead-pi.0.6.6.json
|
||||
review:
|
||||
date: 2026-10-06
|
||||
note: Reviewed for listing. Validation and plan checks are not native runtime
|
||||
proof or a security audit.
|
||||
status: listed
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema": "openrig.journey/v1",
|
||||
"id": "contributor-team",
|
||||
"version": 1,
|
||||
"kind": "team",
|
||||
"title": "The contributor team, from install to one merged pull request",
|
||||
"requiredSteps": [
|
||||
{ "id": "install", "description": "The bundle installs on the machine from its link or archive." },
|
||||
{ "id": "launch", "description": "Every seat launches and answers its startup check." },
|
||||
{ "id": "orient", "description": "The seats orient from the public OpenRig world and resolve the bundle's project." },
|
||||
{ "id": "pull-request", "description": "The team opens one useful pull request." },
|
||||
{ "id": "merged", "description": "That pull request merges after normal review." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"schema": "openrig.journey/v1",
|
||||
"id": "contributor-team",
|
||||
"version": 2,
|
||||
"kind": "team",
|
||||
"title": "The contributor team, from install to a pull request ready to submit",
|
||||
"requiredSteps": [
|
||||
{ "id": "install", "description": "The bundle installs on the machine from its link or archive." },
|
||||
{ "id": "launch", "description": "Every seat launches and answers its startup check." },
|
||||
{ "id": "orient", "description": "The seats orient from the public OpenRig world and resolve the bundle's project." },
|
||||
{ "id": "ready-to-submit", "description": "The team's own review and tests pass, the pull request is drafted, and the lead stops for the person's word to publish." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"schema": "openrig.journey/v1",
|
||||
"id": "harness-check",
|
||||
"version": 1,
|
||||
"kind": "harness_check",
|
||||
"title": "A harness OpenRig can drive on this machine",
|
||||
"requiredSteps": [
|
||||
{ "id": "on-path", "description": "The harness is installed and on PATH." },
|
||||
{ "id": "version-supported", "description": "Its version is one OpenRig drives." },
|
||||
{ "id": "signed-in", "description": "It is signed in and answers a request." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"bodyDigest": "d0595ef3b4ad8ec38fd72b86af8cdab6365625bce3842a7418fd40f9cb34ca86",
|
||||
"generated": {
|
||||
"by": "openrig-world tools/status.mjs",
|
||||
"generatorVersion": 1,
|
||||
"notice": "Generated file. Do not edit by hand; regenerate it.",
|
||||
"rule": "openrig.status-rule/v1"
|
||||
},
|
||||
"harnessChecks": {
|
||||
"harnesses": {},
|
||||
"state": "ok"
|
||||
},
|
||||
"listings": {
|
||||
"factory-rsi": {
|
||||
"configurations": {
|
||||
"build.implementer=claude-code,check.qa=codex,dogfood.tester=codex,orch.lead=claude-code,plan.planner=claude-code,release.manager=claude-code,review.reviewer=codex": {
|
||||
"communityReports": 0,
|
||||
"platforms": {}
|
||||
}
|
||||
},
|
||||
"state": "ok"
|
||||
},
|
||||
"workshop": {
|
||||
"configurations": {
|
||||
"dev.build=claude-code,dev.qa=claude-code,dev.review=claude-code,orch.lead=claude-code": {
|
||||
"communityReports": 0,
|
||||
"platforms": {}
|
||||
},
|
||||
"dev.build=claude-code,dev.qa=claude-code,dev.review=codex,orch.lead=codex": {
|
||||
"communityReports": 0,
|
||||
"platforms": {}
|
||||
},
|
||||
"dev.build=codex,dev.qa=codex,dev.review=codex,orch.lead=codex": {
|
||||
"communityReports": 0,
|
||||
"platforms": {}
|
||||
},
|
||||
"dev.build=pi,dev.qa=pi,dev.review=pi,orch.lead=pi": {
|
||||
"communityReports": 0,
|
||||
"platforms": {}
|
||||
}
|
||||
},
|
||||
"state": "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# Rig catalog tools
|
||||
|
||||
Small Node programs that check the registry and generate its public status. They read files in this repository
|
||||
and, for the generator, run records and receipts on the machine running it. They do not fetch, install or run
|
||||
anything a submitted bundle contains.
|
||||
|
||||
| Program | What it does | Where it runs |
|
||||
|---|---|---|
|
||||
| `registry-check.mjs` | Checks `registry/*.yaml`, each entry's behaviour views, `registry/submissions/`, `status/journeys/` and `status/status.json` | CI on every pull request and push to main |
|
||||
| `status.mjs` | Generates `status/status.json` from run records, or checks that the committed file equals regenerated output | Wherever the run records and their receipts are kept |
|
||||
|
||||
```sh
|
||||
cd tools && npm ci --ignore-scripts
|
||||
node --test test/*.test.mjs
|
||||
node registry-check.mjs
|
||||
node status.mjs generate --records <folder> [--records <folder> ...]
|
||||
node status.mjs check --records <folder> [--records <folder> ...]
|
||||
```
|
||||
|
||||
## Formats
|
||||
|
||||
The formats are defined in the OpenRig repository's `docs/reference/bundle-formats.md`. This folder keeps a copy of
|
||||
the schemas it validates against in `schemas/product/`, pinned in `schemas/product/source.json` by commit and SHA-256.
|
||||
The check refuses a copy that differs from its pin. To update the copy, fetch the schemas at the new commit, then
|
||||
update `commit` and each file's `sha256` in `source.json` in the same pull request.
|
||||
|
||||
Journeys are defined here, in `status/journeys/<id>-v<version>.json` (schema: `schemas/journey.v1.schema.json`). A
|
||||
journey lists the steps a run must pass. Its `kind` is `team` (a whole team, which can earn a label) or
|
||||
`harness_check` (one harness on one machine, shown as its own line and never a team label).
|
||||
|
||||
## Run records
|
||||
|
||||
Run records are private. They're kept beside the receipts they rest on, in a `run-records/` folder:
|
||||
|
||||
```
|
||||
<run folder>/
|
||||
<receipt files>
|
||||
run-records/
|
||||
<listing slug>/<record id>.json team runs for that listing
|
||||
_harness/<record id>.json harness checks
|
||||
```
|
||||
|
||||
A record's `evidence.receipt.ref` is relative to the run folder. The generator reads the receipt and compares its
|
||||
SHA-256. Only the fields listed in the status schema reach `status.json`. Of a record's own text, only
|
||||
`outcome.publicNote` does. The generator refuses to write a status file containing:
|
||||
- an absolute path: any token that starts with one or more `/` and a non-space character, `~/`, or a drive letter with `:\` or `:/`,
|
||||
quoted or not, with any characters after it. A slash inside a word or a URL doesn't start one. Known limits:
|
||||
`path:/…` and `file:///…` pass, because the `:` that lets `https://` through also lets them through;
|
||||
- an address;
|
||||
- a queue row id;
|
||||
- a private host name (`.local`, `.lan`, `.internal`);
|
||||
- an IP address;
|
||||
- a non-GitHub URL;
|
||||
- text that isn't well-formed Unicode, which canonical JSON can't carry.
|
||||
|
||||
Plain names, a person's or a machine's, can't be recognised. Keep public notes short and factual: maintainers read
|
||||
every status pull request.
|
||||
|
||||
## The status rule (`openrig.status-rule/v1`)
|
||||
|
||||
Labels are per listing, configuration and environment: the run's platform and architecture as Node reports them
|
||||
(`process.platform`-`process.arch`, for example `linux-x64`; the status format allows `linux`, `darwin` or `win32` with
|
||||
`x64` or `arm64`). In order:
|
||||
|
||||
1. **Readability.** If any record for a listing can't be read, isn't a v1 run record, names a journey that doesn't
|
||||
exist, or ran on an environment outside that set, the whole listing is `status_unavailable`. That's decided first,
|
||||
whatever the record's evidence kind, so a damaged record never reads as "Not tested". An unreadable harness-check
|
||||
record makes `harnessChecks` unavailable in the same way.
|
||||
2. **Relations and receipts, settled per scope.**
|
||||
- A scope is what one label covers: a listing's configuration (its source, configuration ID and accepted package
|
||||
digests) on one platform, among records of a kind that can earn that label (`native-workflow` for team labels).
|
||||
For harness lines, it's one harness on one platform, among the kinds a harness line uses.
|
||||
- A `withdraws`, `supersedes` or `resolves` counts only from a record in the same scope. A record that could never
|
||||
earn the label (another kind, configuration or platform) can't withdraw, replace or resolve anything in it; its
|
||||
relation is ignored, not an error. To correct a mistaken FAIL, write an eligible record for the same scope.
|
||||
- Records that match no offered configuration play no part.
|
||||
- A record named in another record's `supersedes` or `withdraws` supports nothing.
|
||||
- A withdrawal counts only from a record that isn't itself withdrawn. So withdrawing a withdrawal restores what it
|
||||
withdrew, and a withdrawn record can't clear a FAIL. Withdrawals that form a cycle make the set unavailable.
|
||||
- A supersession counts from any record that isn't withdrawn, so a chain of corrections stays replaced.
|
||||
- Every record still in force in a scope must have its receipt, unchanged. If one is missing or changed, the listing
|
||||
(or `harnessChecks`) is `status_unavailable`, because that record might be the FAIL that decides the label. Lost
|
||||
evidence is never dropped quietly; to clear it, withdraw or supersede the record. A withdrawn or superseded
|
||||
record's receipt may be gone, and so may the receipt of a record that can't label. A community report whose
|
||||
receipt is gone drops out of `communityReports`, because a count isn't a label.
|
||||
3. **Match.** A record applies to a configuration when its source repository and folder, configuration ID and package
|
||||
digest match the listing. A record for another package applies only through that configuration's `evidenceReuse`,
|
||||
and then the label shows both digests.
|
||||
4. **Evidence kinds.**
|
||||
- Only `native-workflow` team records earn a team label.
|
||||
- `synthetic` (a test fixture), `installed-only` (a binary on disk), `daemon-adoption` and `running-seat` (version
|
||||
bindings) aren't evidence that a team ran. They're read and must be readable, but they never label, so the
|
||||
configuration reads Not tested by OpenRig.
|
||||
- `community-reported` records are counted in `communityReports` and never label.
|
||||
- Harness-check lines use every kind except `synthetic` and `community-reported`: a fixture is never evidence about
|
||||
a user's harness, and a community report isn't OpenRig's result. The on-path and version steps can rest on
|
||||
`installed-only` or `running-seat` evidence.
|
||||
5. **Label.**
|
||||
- `known_problem`: a FAIL that's unresolved. A FAIL is resolved only by a record in force that names it in
|
||||
`resolves`, passes that step, and ran on the same or a newer OpenRig version (SemVer precedence:
|
||||
prereleases ordered, build metadata ignored).
|
||||
- Otherwise the best record decides: `tested` (every required step passed, with no help), `tested_with_help`
|
||||
(every required step passed, with help), `partly_tested` (some passed). A tie goes to the newest.
|
||||
- **The best record wins on purpose.** A newer partial or assisted run doesn't erase an older complete,
|
||||
unassisted pass on the same package and environment. When a newer run should replace an older one, record it
|
||||
with `supersedes`.
|
||||
- With no such record, `platforms` is empty, which means Not tested by OpenRig.
|
||||
- A harness-check line shows the newest harness-check record in force for that harness and environment.
|
||||
|
||||
The OpenRig version is shown, not matched: a label carries forward to later releases, dated, until new evidence
|
||||
changes it. Each label carries the date, OpenRig version, record ids and package digest(s) it rests on. A Known
|
||||
problem or Partly tested label also carries the deciding record's `publicNote` as `note`, when it has one.
|
||||
|
||||
## Wording
|
||||
|
||||
The site shows the labels as: Known problem, Tested by OpenRig, Tested with help (N), Partly tested,
|
||||
Not tested by OpenRig, and Status unavailable.
|
||||
|
||||
## `bodyDigest`
|
||||
|
||||
`status.json` ends with `bodyDigest`, the SHA-256 of the canonical JSON (RFC 8785) of every other top-level field. A
|
||||
hand edit that doesn't regenerate the file fails the registry check. The digest catches mistakes; it isn't a
|
||||
signature.
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
/**
|
||||
* JSON with object keys sorted by UTF-16 code unit at every depth and no whitespace. For the data these files
|
||||
* hold (strings, integers, objects, arrays) this is RFC 8785 (JCS) byte for byte.
|
||||
*/
|
||||
export function canonicalJson(value) {
|
||||
// Built as a string: a rebuilt object would move integer-like keys (a slug such as "42") back to the front.
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (value && typeof value === "object") {
|
||||
const keys = Object.keys(value).filter((key) => value[key] !== undefined).sort();
|
||||
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
/** The committed form of a generated file: sorted keys, two-space indent, trailing newline. */
|
||||
export function canonicalFile(value) {
|
||||
return `${JSON.stringify(sortKeys(value), null, 2)}\n`;
|
||||
}
|
||||
|
||||
function sortKeys(value) {
|
||||
if (Array.isArray(value)) return value.map(sortKeys);
|
||||
if (value && typeof value === "object") {
|
||||
const out = {};
|
||||
for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key]);
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function sha256Hex(data) {
|
||||
return createHash("sha256").update(data).digest("hex");
|
||||
}
|
||||
|
||||
/** The status file's self-digest: canonical JSON of everything except `bodyDigest`. */
|
||||
export function statusBodyDigest(status) {
|
||||
const { bodyDigest: _ignored, ...body } = status;
|
||||
return sha256Hex(canonicalJson(body));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Configuration ID (bundle-formats.md): every member's `pod.member=runtime`, sorted by `pod.member` in plain
|
||||
// code-unit order, joined with "," and no whitespace. Preset names are aliases beside the ID, never inside it.
|
||||
const PAIR = /^([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)=([a-z0-9][a-z0-9-]*)$/;
|
||||
|
||||
/** Returns null when `id` is canonical, otherwise a reason. */
|
||||
export function configurationIdProblem(id) {
|
||||
if (typeof id !== "string" || id.length === 0) return "empty configuration ID";
|
||||
const keys = [];
|
||||
for (const pair of id.split(",")) {
|
||||
const match = PAIR.exec(pair);
|
||||
if (!match) return `"${pair}" is not pod.member=runtime`;
|
||||
keys.push(match[1]);
|
||||
}
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
if (keys[i - 1] === keys[i]) return `member ${keys[i]} appears twice`;
|
||||
if (keys[i - 1] > keys[i]) return `members are not sorted (${keys[i - 1]} before ${keys[i]})`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Reads registry entries, journeys, run records and receipts from disk. Local files only: nothing here
|
||||
// fetches, executes or launches anything.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
import { parseDocument } from "yaml";
|
||||
import { sha256Hex } from "./canonical.mjs";
|
||||
|
||||
const SCHEMAS = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "schemas");
|
||||
const SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
||||
export const HARNESS_FOLDER = "_harness";
|
||||
|
||||
/**
|
||||
* Compiles the vendored product schemas after checking each file against the pin in
|
||||
* schemas/product/source.json, plus the journey schema this repository owns.
|
||||
*/
|
||||
export function loadValidators() {
|
||||
const pin = JSON.parse(fs.readFileSync(path.join(SCHEMAS, "product", "source.json"), "utf8"));
|
||||
const problems = [];
|
||||
const ajv = new Ajv2020({ strict: false, allErrors: false });
|
||||
addFormats(ajv, ["date", "date-time"]);
|
||||
for (const [name, expected] of Object.entries(pin.files)) {
|
||||
const bytes = fs.readFileSync(path.join(SCHEMAS, "product", name));
|
||||
const actual = sha256Hex(bytes);
|
||||
if (actual !== expected) problems.push(`schemas/product/${name}: sha256 ${actual} does not match the pin ${expected}`);
|
||||
ajv.addSchema(JSON.parse(bytes.toString("utf8")));
|
||||
}
|
||||
const journey = JSON.parse(fs.readFileSync(path.join(SCHEMAS, "journey.v1.schema.json"), "utf8"));
|
||||
ajv.addSchema(journey);
|
||||
const submission = JSON.parse(fs.readFileSync(path.join(SCHEMAS, "registry-submission.v1.schema.json"), "utf8"));
|
||||
ajv.addSchema(submission);
|
||||
const get = (id) => ajv.getSchema(id);
|
||||
return {
|
||||
problems,
|
||||
runRecord: get("https://openrig.dev/schemas/run-record.v1.json"),
|
||||
status: get("https://openrig.dev/schemas/bundle-status.v1.json"),
|
||||
registryEntry: get("https://openrig.dev/schemas/registry-entry.v1.json"),
|
||||
behaviour: get("https://openrig.dev/schemas/bundle-behaviour.v1.json"),
|
||||
journey: get(journey.$id),
|
||||
submission: get(submission.$id),
|
||||
};
|
||||
}
|
||||
|
||||
/** The first schema error as one readable line. */
|
||||
export function schemaError(validate) {
|
||||
const e = validate.errors?.[0];
|
||||
return e ? `${e.instancePath || "(root)"} ${e.message}` : "invalid";
|
||||
}
|
||||
|
||||
export function readYaml(file) {
|
||||
const doc = parseDocument(fs.readFileSync(file, "utf8"), { uniqueKeys: true });
|
||||
if (doc.errors.length) throw new Error(doc.errors[0].message.split("\n")[0]);
|
||||
return doc.toJS();
|
||||
}
|
||||
|
||||
/** `registry/*.yaml` as `{ file, entry?, problem? }`, sorted by file name. */
|
||||
export function loadRegistry(root, validators) {
|
||||
const dir = path.join(root, "registry");
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir).filter((name) => name.endsWith(".yaml")).sort().map((name) => {
|
||||
const file = `registry/${name}`;
|
||||
let entry;
|
||||
try {
|
||||
entry = readYaml(path.join(dir, name));
|
||||
} catch (error) {
|
||||
return { file, problem: `not readable YAML: ${error.message}` };
|
||||
}
|
||||
if (!validators.registryEntry(entry)) return { file, problem: `not a registry entry v1: ${schemaError(validators.registryEntry)}` };
|
||||
return { file, entry };
|
||||
});
|
||||
}
|
||||
|
||||
export const SUBMISSION_SHAPE =
|
||||
"a submission has exactly three fields: repository (https://github.com/<owner>/<repo>), folder (the folder holding " +
|
||||
"rig.yaml, or . for the repository root) and ref (a branch, tag or commit)";
|
||||
|
||||
/**
|
||||
* `registry/submissions/*.yaml` as `{ file, submission?, problem? }`, sorted by file name. A submission is a request
|
||||
* for listing, never a listing: loadRegistry, the status generator and the site read only `registry/*.yaml`.
|
||||
*/
|
||||
export function loadSubmissions(root, validators) {
|
||||
const dir = path.join(root, "registry", "submissions");
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir).filter((name) => name !== "README.md").sort().map((name) => {
|
||||
// Names are echoed into CI output (and a notice), so only plain names are read; any other is quoted.
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.ya?ml$/.test(name)) {
|
||||
return { file: `registry/submissions/${JSON.stringify(name)}`, problem: `name the file <your-team>.yaml (letters, digits, ".", "_", "-"); ${SUBMISSION_SHAPE}` };
|
||||
}
|
||||
const file = `registry/submissions/${name}`;
|
||||
let submission;
|
||||
try {
|
||||
submission = readYaml(path.join(dir, name));
|
||||
} catch (error) {
|
||||
return { file, problem: `not readable YAML: ${error.message}` };
|
||||
}
|
||||
if (!validators.submission(submission)) return { file, problem: `${SUBMISSION_SHAPE} (${schemaError(validators.submission)})` };
|
||||
return { file, submission };
|
||||
});
|
||||
}
|
||||
|
||||
/** `status/journeys/<id>-v<version>.json` as a Map "id@version" -> journey, plus problems. */
|
||||
export function loadJourneys(root, validators) {
|
||||
const dir = path.join(root, "status", "journeys");
|
||||
const journeys = new Map();
|
||||
const problems = [];
|
||||
if (!fs.existsSync(dir)) return { journeys, problems };
|
||||
for (const name of fs.readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
|
||||
const file = `status/journeys/${name}`;
|
||||
let journey;
|
||||
try {
|
||||
journey = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
||||
} catch (error) {
|
||||
problems.push(`${file}: not readable JSON: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
if (!validators.journey(journey)) {
|
||||
problems.push(`${file}: not a journey v1: ${schemaError(validators.journey)}`);
|
||||
continue;
|
||||
}
|
||||
if (name !== `${journey.id}-v${journey.version}.json`) problems.push(`${file}: name must be ${journey.id}-v${journey.version}.json`);
|
||||
const steps = journey.requiredSteps.map((s) => s.id);
|
||||
if (new Set(steps).size !== steps.length) problems.push(`${file}: a required step id appears twice`);
|
||||
journeys.set(`${journey.id}@${journey.version}`, journey);
|
||||
}
|
||||
return { journeys, problems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run records under each root: every `run-records/<slug>/*.json` (team records for that listing) and
|
||||
* `run-records/_harness/*.json` (harness checks). A record's receipt ref is relative to the run folder,
|
||||
* the parent of `run-records/`; it must exist there with the recorded sha256.
|
||||
*/
|
||||
export function loadRecords(roots, validators) {
|
||||
const teamFiles = new Map();
|
||||
const harnessFiles = [];
|
||||
const warnings = [];
|
||||
for (const root of roots) {
|
||||
for (const recordsDir of findRecordFolders(root)) {
|
||||
const runDir = path.dirname(recordsDir);
|
||||
for (const group of fs.readdirSync(recordsDir, { withFileTypes: true })) {
|
||||
const groupDir = path.join(recordsDir, group.name);
|
||||
if (!group.isDirectory()) continue;
|
||||
if (group.name !== HARNESS_FOLDER && !SLUG.test(group.name)) {
|
||||
warnings.push(`${path.relative(root, groupDir)}: not a listing slug or ${HARNESS_FOLDER}; its records were not read`);
|
||||
continue;
|
||||
}
|
||||
for (const name of fs.readdirSync(groupDir).filter((n) => n.endsWith(".json")).sort()) {
|
||||
const loaded = loadRecord(path.join(groupDir, name), runDir, validators);
|
||||
loaded.file = path.relative(root, path.join(groupDir, name));
|
||||
if (group.name === HARNESS_FOLDER) harnessFiles.push(loaded);
|
||||
else teamFiles.set(group.name, [...(teamFiles.get(group.name) ?? []), loaded]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { teamFiles, harnessFiles, warnings };
|
||||
}
|
||||
|
||||
function loadRecord(file, runDir, validators) {
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (error) {
|
||||
return { problem: `not readable JSON: ${error.message}`, receiptVerified: false };
|
||||
}
|
||||
if (!validators.runRecord(record)) {
|
||||
return { problem: `not a run record v1: ${schemaError(validators.runRecord)}`, receiptVerified: false };
|
||||
}
|
||||
return { record, receiptVerified: receiptMatches(runDir, record.evidence.receipt) };
|
||||
}
|
||||
|
||||
function receiptMatches(runDir, receipt) {
|
||||
const target = path.resolve(runDir, receipt.ref);
|
||||
if (path.isAbsolute(receipt.ref) || !target.startsWith(runDir + path.sep)) return false;
|
||||
try {
|
||||
return sha256Hex(fs.readFileSync(target)) === receipt.sha256;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findRecordFolders(root) {
|
||||
const found = [];
|
||||
const walk = (dir) => {
|
||||
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (!item.isDirectory() || item.name === "node_modules" || item.name === ".git") continue;
|
||||
const full = path.join(dir, item.name);
|
||||
if (item.name === "run-records") found.push(full);
|
||||
else walk(full);
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return found.sort();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Classes of private detail that must never reach a public status file. The generator builds its output
|
||||
// from an allowlist of fields, so these are a second line: they catch private text inside an allowed
|
||||
// free-text field such as a public note.
|
||||
export const PRIVATE_PATTERNS = [
|
||||
// Any token that starts like a path: one or more "/" and a non-space character, "~/", or a drive letter with ":\" or
|
||||
// ":/", whatever follows (spaces inside quotes, Unicode). It starts a token unless the character before it is
|
||||
// a letter, digit or one of _ / : . ~ - (Unicode letters and digits included), so "and/or", "1/2",
|
||||
// "résumé/données" and https://github.com/... pass, while **bold**, 2>/redirects, |pipes and typographic
|
||||
// quotes don't hide a path. Known limits: "path:/x" and "file:///x" pass, because ":" is what lets "https:" pass.
|
||||
{ kind: "absolute path", re: /(^|[^\p{L}\p{N}_\/:.~-])(\/+[^\s\/]|~\/|[A-Za-z]:[\\/])/u },
|
||||
{ kind: "account, session or email address", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*/ },
|
||||
{ kind: "queue row id", re: /\bqitem-/i },
|
||||
{ kind: "private host name", re: /\b[a-z0-9-]+\.(local|lan|internal|localdomain|home\.arpa)\b/i },
|
||||
{ kind: "IP address or localhost", re: /\b(\d{1,3}(\.\d{1,3}){3}|localhost)\b/i },
|
||||
{ kind: "non-GitHub URL", re: /\bhttps?:\/\/(?!github\.com\/)/i },
|
||||
];
|
||||
|
||||
/** Every string in `value` that matches a private class, as `{ at, kind }` with a JSON-path-like location. */
|
||||
export function findPrivateText(value, at = "$") {
|
||||
const found = [];
|
||||
if (typeof value === "string") {
|
||||
for (const { kind, re } of PRIVATE_PATTERNS) if (re.test(value)) found.push({ at, kind });
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((item, i) => found.push(...findPrivateText(item, `${at}[${i}]`)));
|
||||
} else if (value && typeof value === "object") {
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
found.push(...findPrivateText(key, `${at}{key}`));
|
||||
found.push(...findPrivateText(item, `${at}.${key}`));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Every string (keys included) that isn't well-formed Unicode, which RFC 8785 canonical JSON can't carry. */
|
||||
export function findIllFormedText(value, at = "$") {
|
||||
if (typeof value === "string") return value.isWellFormed() ? [] : [at];
|
||||
if (Array.isArray(value)) return value.flatMap((item, i) => findIllFormedText(item, `${at}[${i}]`));
|
||||
if (value && typeof value === "object") {
|
||||
return Object.entries(value).flatMap(([key, item]) => [...findIllFormedText(key, `${at}{key}`), ...findIllFormedText(item, `${at}.${key}`)]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// openrig.status-rule/v1: derives every public tested label from run records. Pure: the loader reads files,
|
||||
// checks receipts and validates schemas; this module only decides. The rule in words is in tools/README.md.
|
||||
import { statusBodyDigest } from "./canonical.mjs";
|
||||
|
||||
export const RULE_ID = "openrig.status-rule/v1";
|
||||
export const GENERATOR_VERSION = 1;
|
||||
export const GENERATED = Object.freeze({
|
||||
by: "openrig-world tools/status.mjs",
|
||||
notice: "Generated file. Do not edit by hand; regenerate it.",
|
||||
rule: RULE_ID,
|
||||
generatorVersion: GENERATOR_VERSION,
|
||||
});
|
||||
|
||||
const RANK = { tested: 3, tested_with_help: 2, partly_tested: 1 };
|
||||
// Only a real team run earns a team label. Other kinds are read and must be readable, but never label.
|
||||
const TEAM_LABEL_KINDS = new Set(["native-workflow"]);
|
||||
// A stub can't show a real harness works, and a community report is never OpenRig's own result.
|
||||
const HARNESS_LINE_EXCLUDED_KINDS = new Set(["synthetic", "community-reported"]);
|
||||
// Labels are per environment: Node's platform and architecture, as the status schema spells them.
|
||||
const PLATFORM = /^(linux|darwin|win32)$/;
|
||||
const ARCH = /^(x64|arm64)$/;
|
||||
const environmentOf = (r) => `${r.record.environment.platform}-${r.record.environment.arch}`;
|
||||
|
||||
/**
|
||||
* @param entries registry entries (`slug` added by the loader); only `listed` ones get labels
|
||||
* @param teamFiles Map slug -> [{ file, record?, problem?, receiptVerified }]
|
||||
* @param harnessFiles [{ file, record?, problem?, receiptVerified }]
|
||||
* @param journeys Map "id@version" -> journey
|
||||
* @returns { status, problems } where problems explain every "status_unavailable" (they stay private)
|
||||
*/
|
||||
export function deriveStatus({ entries, teamFiles, harnessFiles, journeys }) {
|
||||
const problems = [];
|
||||
const listings = {};
|
||||
for (const entry of entries.filter((e) => e.status === "listed")) {
|
||||
const derived = deriveListing(entry, teamFiles.get(entry.slug) ?? [], journeys);
|
||||
if (derived.problem) {
|
||||
problems.push(`${entry.slug}: ${derived.problem}`);
|
||||
listings[entry.slug] = { state: "status_unavailable", configurations: {} };
|
||||
} else {
|
||||
listings[entry.slug] = { state: "ok", configurations: derived.configurations };
|
||||
}
|
||||
}
|
||||
const harness = deriveHarnessChecks(harnessFiles, journeys);
|
||||
if (harness.problem) problems.push(`harness checks: ${harness.problem}`);
|
||||
const harnessChecks = harness.problem
|
||||
? { state: "status_unavailable", harnesses: {} }
|
||||
: { state: "ok", harnesses: harness.harnesses };
|
||||
const status = { generated: { ...GENERATED }, listings, harnessChecks };
|
||||
status.bodyDigest = statusBodyDigest(status);
|
||||
return { status, problems };
|
||||
}
|
||||
|
||||
// Step 2 for one scope: the records that could label it, settled among themselves. A relation counts only
|
||||
// from a record in the same scope, so a record that could never earn this label can't change it. Lost
|
||||
// evidence is never dropped quietly: a record in force whose receipt is missing or changed might be the
|
||||
// FAIL that decides the label, so it makes the set unavailable (withdraw or supersede it to clear this).
|
||||
function settleScope(records, { lostMakesUnavailable = true } = {}) {
|
||||
const relations = relationsInForce(records);
|
||||
if (relations.problem) return relations;
|
||||
const live = records.filter((r) => !relations.gone.has(r.record.id));
|
||||
const lost = live.find((r) => !r.receiptVerified);
|
||||
if (lost && lostMakesUnavailable) return { problem: `${lost.file}: its receipt is missing or changed` };
|
||||
return { live: live.filter((r) => r.receiptVerified) };
|
||||
}
|
||||
|
||||
// Step 1, readability, decided before anything else: one record we can't read or interpret makes the
|
||||
// whole set unavailable, because it might have been a FAIL.
|
||||
function readable(files, kind, journeys) {
|
||||
const records = [];
|
||||
const ids = new Set();
|
||||
for (const f of files) {
|
||||
if (f.problem) return { problem: `${f.file}: ${f.problem}` };
|
||||
const r = f.record;
|
||||
const journey = journeys.get(`${r.journey.id}@${r.journey.version}`);
|
||||
if (!journey) return { problem: `${f.file}: unknown journey ${r.journey.id} v${r.journey.version}` };
|
||||
if (journey.kind !== kind) return { problem: `${f.file}: journey ${journey.id} is ${journey.kind}, not ${kind}` };
|
||||
if (r.subject.kind !== kind) return { problem: `${f.file}: a ${r.subject.kind} subject on a ${kind} journey` };
|
||||
if (!PLATFORM.test(r.environment.platform) || !ARCH.test(r.environment.arch)) {
|
||||
return { problem: `${f.file}: environment ${r.environment.platform}-${r.environment.arch} is not linux|darwin|win32 with x64|arm64` };
|
||||
}
|
||||
if (ids.has(r.id)) return { problem: `record id ${r.id} appears twice` };
|
||||
ids.add(r.id);
|
||||
records.push({ ...f, journey });
|
||||
}
|
||||
return { records };
|
||||
}
|
||||
|
||||
// Step 2, relations. A withdrawal counts only from a record that isn't itself withdrawn, so withdrawing a
|
||||
// withdrawal restores what it withdrew, and a withdrawn record can't clear a FAIL. That's a fixed point; a
|
||||
// cycle that never settles makes the set unavailable. A supersession counts from any record not withdrawn,
|
||||
// so a chain of corrections stays replaced.
|
||||
function relationsInForce(records) {
|
||||
let withdrawn = new Set();
|
||||
for (let round = 0; round <= 2 * records.length + 2; round++) {
|
||||
const next = new Set();
|
||||
for (const { record } of records) {
|
||||
if (!withdrawn.has(record.id)) for (const id of record.relations.withdraws) next.add(id);
|
||||
}
|
||||
if (next.size === withdrawn.size && [...next].every((id) => withdrawn.has(id))) {
|
||||
const gone = new Set(withdrawn);
|
||||
for (const { record } of records) {
|
||||
if (!withdrawn.has(record.id)) for (const id of record.relations.supersedes) gone.add(id);
|
||||
}
|
||||
return { gone };
|
||||
}
|
||||
withdrawn = next;
|
||||
}
|
||||
return { problem: "its withdrawals form a cycle" };
|
||||
}
|
||||
|
||||
// Step 3, match: same source folder, configuration and package. A different package counts only through
|
||||
// the listing's explicit `evidenceReuse`, and then both digests are shown. Each platform's eligible records
|
||||
// are one scope for step 2. A record that matches no offered configuration plays no part at all.
|
||||
function deriveListing(entry, files, journeys) {
|
||||
const read = readable(files, "team", journeys);
|
||||
if (read.problem) return read;
|
||||
const configurations = {};
|
||||
for (const cfg of entry.configurations) {
|
||||
const current = cfg.packageDigest;
|
||||
const accepted = new Set([current.value, ...(cfg.evidenceReuse ?? []).map((reuse) => reuse.packageDigest.value)]);
|
||||
const matching = read.records.filter(({ record: { subject } }) =>
|
||||
subject.source.repository === entry.source.repository &&
|
||||
subject.source.folder === entry.source.folder &&
|
||||
subject.configurationId === cfg.id &&
|
||||
accepted.has(subject.packageDigest.value));
|
||||
const ours = matching.filter((r) => TEAM_LABEL_KINDS.has(r.record.evidence.kind));
|
||||
const platforms = {};
|
||||
for (const platform of unique(ours.map(environmentOf))) {
|
||||
const settled = settleScope(ours.filter((r) => environmentOf(r) === platform));
|
||||
if (settled.problem) return { problem: `${cfg.id} on ${platform}: ${settled.problem}` };
|
||||
const label = labelFor(settled.live, current);
|
||||
if (label) platforms[platform] = label;
|
||||
}
|
||||
// Community reports settle among themselves for the count. A lost receipt leaves a report out of the
|
||||
// count but never makes the listing unavailable, because a count isn't a label.
|
||||
const community = settleScope(matching.filter((r) => r.record.evidence.kind === "community-reported"), { lostMakesUnavailable: false });
|
||||
if (community.problem) return { problem: `${cfg.id} community reports: ${community.problem}` };
|
||||
configurations[cfg.id] = { platforms, communityReports: community.live.length };
|
||||
}
|
||||
return { configurations };
|
||||
}
|
||||
|
||||
// Step 4, label, per platform.
|
||||
function labelFor(records, current) {
|
||||
const failing = records.filter((r) =>
|
||||
r.record.outcome.steps.some((s) => s.result === "FAIL" && !isResolved(r, s.id, records)));
|
||||
if (failing.length) return labelEntry("known_problem", failing, current);
|
||||
let best = null;
|
||||
for (const r of records) {
|
||||
const level = levelOf(r);
|
||||
if (!level) continue;
|
||||
if (!best || RANK[level] > RANK[best.level] || (RANK[level] === RANK[best.level] && newer(r, best.r))) best = { level, r };
|
||||
}
|
||||
return best && labelEntry(best.level, [best.r], current);
|
||||
}
|
||||
|
||||
// A FAIL is resolved only by an applicable, non-withdrawn record that names it in `resolves`, PASSes that
|
||||
// step, and ran on the same or a newer OpenRig version. `records` is already applicable and in force.
|
||||
function isResolved(failing, stepId, records) {
|
||||
return records.some((q) =>
|
||||
q !== failing &&
|
||||
(q.record.relations?.resolves ?? []).some((x) => x.record === failing.record.id && x.step === stepId) &&
|
||||
stepResult(q, stepId) === "PASS" &&
|
||||
compareVersions(q.record.environment.openrig.version, failing.record.environment.openrig.version) >= 0);
|
||||
}
|
||||
|
||||
function levelOf(r) {
|
||||
const required = r.journey.requiredSteps.map((s) => s.id);
|
||||
if (required.every((id) => stepResult(r, id) === "PASS")) {
|
||||
return r.record.outcome.assistance.count > 0 ? "tested_with_help" : "tested";
|
||||
}
|
||||
return required.some((id) => stepResult(r, id) === "PASS") ? "partly_tested" : null;
|
||||
}
|
||||
|
||||
function labelEntry(label, records, current) {
|
||||
const newest = records.reduce((a, b) => (newer(b, a) ? b : a));
|
||||
const tested = new Map();
|
||||
for (const { record: { subject } } of records) {
|
||||
if (subject.packageDigest.value !== current.value) tested.set(subject.packageDigest.value, subject.packageDigest);
|
||||
}
|
||||
const out = {
|
||||
label,
|
||||
date: utcDate(newest),
|
||||
openrigVersion: newest.record.environment.openrig.version,
|
||||
recordIds: unique(records.map((r) => r.record.id)),
|
||||
packageDigests: [current, ...[...tested.keys()].sort().map((value) => tested.get(value))],
|
||||
};
|
||||
if (label === "tested_with_help") out.assistanceCount = newest.record.outcome.assistance.count;
|
||||
// A note says what failed (known problem) or what a partial run left out (partly tested).
|
||||
if ((label === "known_problem" || label === "partly_tested") && newest.record.outcome.publicNote) out.note = newest.record.outcome.publicNote;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Harness checks never make a team label; they're a separate line per harness and platform, from the
|
||||
// newest record in force. Each harness and platform's eligible records are one scope for step 2.
|
||||
function deriveHarnessChecks(files, journeys) {
|
||||
const read = readable(files, "harness_check", journeys);
|
||||
if (read.problem) return read;
|
||||
const eligible = read.records.filter((r) => !HARNESS_LINE_EXCLUDED_KINDS.has(r.record.evidence.kind));
|
||||
const harnesses = {};
|
||||
for (const harness of unique(eligible.map((r) => r.record.subject.harness))) {
|
||||
const mine = eligible.filter((r) => r.record.subject.harness === harness);
|
||||
for (const platform of unique(mine.map(environmentOf))) {
|
||||
const settled = settleScope(mine.filter((r) => environmentOf(r) === platform));
|
||||
if (settled.problem) return { problem: `${harness} on ${platform}: ${settled.problem}` };
|
||||
if (!settled.live.length) continue;
|
||||
const newest = settled.live.reduce((a, b) => (newer(b, a) ? b : a));
|
||||
const line = {
|
||||
result: harnessResult(newest),
|
||||
date: utcDate(newest),
|
||||
openrigVersion: newest.record.environment.openrig.version,
|
||||
harnessVersion: newest.record.environment.harnesses.find((h) => h.name === harness)?.version ?? "unknown",
|
||||
recordIds: [newest.record.id],
|
||||
};
|
||||
if (newest.record.outcome.publicNote) line.note = newest.record.outcome.publicNote;
|
||||
(harnesses[harness] ??= {})[platform] = line;
|
||||
}
|
||||
}
|
||||
return { harnesses };
|
||||
}
|
||||
|
||||
function harnessResult(r) {
|
||||
const results = r.journey.requiredSteps.map((s) => stepResult(r, s.id));
|
||||
if (results.includes("FAIL")) return "FAIL";
|
||||
if (results.every((x) => x === "PASS")) return "PASS";
|
||||
return results.includes("BLOCKED") ? "BLOCKED" : "NOT_RUN";
|
||||
}
|
||||
|
||||
function stepResult(r, stepId) {
|
||||
return r.record.outcome.steps.find((s) => s.id === stepId)?.result ?? "NOT_RUN";
|
||||
}
|
||||
|
||||
function newer(a, b) {
|
||||
const at = Date.parse(a.record.outcome.at) - Date.parse(b.record.outcome.at);
|
||||
return at !== 0 ? at > 0 : a.record.id > b.record.id;
|
||||
}
|
||||
|
||||
function utcDate(r) {
|
||||
return new Date(r.record.outcome.at).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values)].sort();
|
||||
}
|
||||
|
||||
/** Compares release versions; NaN when either can't be read, so an unreadable version never resolves a FAIL. */
|
||||
export function compareVersions(a, b) {
|
||||
// Build metadata (+...) doesn't change precedence, so it's parsed and ignored.
|
||||
const parse = (v) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v);
|
||||
const x = parse(a);
|
||||
const y = parse(b);
|
||||
if (!x || !y) return Number.NaN;
|
||||
for (let i = 1; i <= 3; i++) if (Number(x[i]) !== Number(y[i])) return Number(x[i]) - Number(y[i]);
|
||||
if (x[4] === y[4]) return 0;
|
||||
if (x[4] === undefined) return 1;
|
||||
if (y[4] === undefined) return -1;
|
||||
return comparePrerelease(x[4].split("."), y[4].split("."));
|
||||
}
|
||||
|
||||
// SemVer precedence: numeric identifiers compare as numbers and sort before alphanumeric ones; a shorter
|
||||
// list of otherwise equal identifiers sorts first.
|
||||
function comparePrerelease(x, y) {
|
||||
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
||||
if (x[i] === undefined) return -1;
|
||||
if (y[i] === undefined) return 1;
|
||||
const xNumeric = /^\d+$/.test(x[i]);
|
||||
const yNumeric = /^\d+$/.test(y[i]);
|
||||
if (xNumeric && yNumeric && Number(x[i]) !== Number(y[i])) return Number(x[i]) - Number(y[i]);
|
||||
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
||||
if (!xNumeric && x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Generated
+105
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "openrig-catalog-tools",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig-catalog-tools",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"ajv": "8.18.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"yaml": "2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
},
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "openrig-catalog-tools",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Registry checks and public status generation. Reads local files only.",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test test/*.test.mjs",
|
||||
"check": "node registry-check.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "8.18.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"yaml": "2.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks registry entries, their behaviour views, the journeys and the generated status file. It reads this
|
||||
// repository's files and nothing else: it fetches nothing and runs nothing a submission contains.
|
||||
//
|
||||
// node tools/registry-check.mjs [--root <registry-root>]
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { statusBodyDigest } from "./lib/canonical.mjs";
|
||||
import { configurationIdProblem } from "./lib/config-id.mjs";
|
||||
import { loadJourneys, loadRegistry, loadSubmissions, loadValidators, readYaml, schemaError } from "./lib/load.mjs";
|
||||
import { findIllFormedText, findPrivateText } from "./lib/private-patterns.mjs";
|
||||
import { GENERATED } from "./lib/status-rule.mjs";
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
export const RECEIVED = "Submission received; a maintainer completes the pinned commit, package digests and views before it's listed.";
|
||||
|
||||
/** Every finding as one line; an empty list means the repository passes. */
|
||||
export function checkRepository(root = REPO) {
|
||||
const validators = loadValidators();
|
||||
const findings = [...validators.problems];
|
||||
for (const item of loadRegistry(root, validators)) {
|
||||
if (item.problem) findings.push(misplacedSubmission(root, item.file) ?? `${item.file}: ${item.problem}`);
|
||||
else findings.push(...checkEntry(root, item.file, item.entry, validators));
|
||||
}
|
||||
for (const item of loadSubmissions(root, validators)) if (item.problem) findings.push(`${item.file}: ${item.problem}`);
|
||||
findings.push(...loadJourneys(root, validators).problems);
|
||||
findings.push(...checkStatus(root, validators));
|
||||
return findings;
|
||||
}
|
||||
|
||||
/** Rig names on openrig.dev/rigs are unique; a taken one gets a friendly hint, never a failure. */
|
||||
export const nameTaken = (name) => `That name is taken. Choose a distinct one, for example ${name}-<your-name>.`;
|
||||
|
||||
/** One notice per well-formed submission in `registry/submissions/`. */
|
||||
export function submissionNotices(root = REPO) {
|
||||
const registry = path.join(root, "registry");
|
||||
const taken = new Set(fs.existsSync(registry)
|
||||
? fs.readdirSync(registry).filter((n) => n.endsWith(".yaml")).map((n) => n.slice(0, -".yaml".length).toLowerCase())
|
||||
: []);
|
||||
return loadSubmissions(root, loadValidators()).filter((item) => !item.problem).map((item) => {
|
||||
const name = path.basename(item.file).replace(/\.ya?ml$/, "");
|
||||
return { file: item.file, message: taken.has(name.toLowerCase()) ? `${RECEIVED} ${nameTaken(name)}` : RECEIVED };
|
||||
});
|
||||
}
|
||||
|
||||
// A file here with no configurations can never be an entry; it's most likely a submission in the wrong place, and
|
||||
// the schema's first error would only mislead. Say where it goes instead.
|
||||
function misplacedSubmission(root, file) {
|
||||
try {
|
||||
const value = readYaml(path.join(root, file));
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && !("configurations" in value)) {
|
||||
return `${file}: this isn't a complete registry entry (it has no configurations). To submit a rig for listing, add ` +
|
||||
`registry/submissions/${path.basename(file)} with only repository, folder and ref; a maintainer completes the entry.`;
|
||||
}
|
||||
} catch {
|
||||
// Unreadable YAML: the finding already says so.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkEntry(root, file, entry, validators) {
|
||||
const findings = [];
|
||||
const say = (message) => findings.push(`${file}: ${message}`);
|
||||
if (path.basename(file, ".yaml") !== entry.slug) say(`slug ${entry.slug} must match the file name`);
|
||||
const { repository, folder, resolvedCommit, canonicalUrl } = entry.source;
|
||||
const expectedUrl = `${repository}/tree/${resolvedCommit}${folder === "." ? "" : `/${folder}`}`;
|
||||
if (canonicalUrl !== undefined && canonicalUrl !== expectedUrl) say(`source.canonicalUrl must be ${expectedUrl}`);
|
||||
const ids = new Set();
|
||||
const aliases = new Set();
|
||||
for (const cfg of entry.configurations) {
|
||||
const problem = configurationIdProblem(cfg.id);
|
||||
if (problem) say(`configuration ${cfg.id}: ${problem}`);
|
||||
if (ids.has(cfg.id)) say(`configuration ${cfg.id} appears twice`);
|
||||
ids.add(cfg.id);
|
||||
if (cfg.alias !== undefined && aliases.has(cfg.alias)) say(`alias ${cfg.alias} appears twice`);
|
||||
if (cfg.alias !== undefined) aliases.add(cfg.alias);
|
||||
findings.push(...checkBehaviour(root, `${file}: configuration ${cfg.alias ?? cfg.id}`, cfg, validators));
|
||||
}
|
||||
if (entry.configurations.filter((cfg) => cfg.recommended).length > 1) say("more than one configuration is recommended");
|
||||
return findings;
|
||||
}
|
||||
|
||||
// The schema already refuses absolute and `..` paths; the resolve check below is the same rule said twice.
|
||||
function checkBehaviour(root, where, cfg, validators) {
|
||||
const registryDir = path.join(root, "registry");
|
||||
const target = path.resolve(registryDir, cfg.behaviour);
|
||||
const label = `${where}: behaviour ${cfg.behaviour}`;
|
||||
if (!target.startsWith(registryDir + path.sep)) return [`${label} is outside registry/`];
|
||||
let view;
|
||||
try {
|
||||
// Read only inside registry/, through symlinks too.
|
||||
const real = fs.realpathSync(target);
|
||||
if (!real.startsWith(fs.realpathSync(registryDir) + path.sep)) return [`${label} resolves outside registry/ through a symlink`];
|
||||
view = JSON.parse(fs.readFileSync(real, "utf8"));
|
||||
} catch {
|
||||
return [`${label} is missing or not readable JSON`];
|
||||
}
|
||||
if (!validators.behaviour(view)) return [`${label} is not a behaviour view v1: ${schemaError(validators.behaviour)}`];
|
||||
if (view.state !== "generated") return [];
|
||||
const findings = [];
|
||||
const { configurationId, packageDigest, assembler } = view.identity;
|
||||
if (configurationId !== cfg.id) findings.push(`${label} describes configuration ${configurationId}`);
|
||||
if (packageDigest?.value !== cfg.packageDigest.value) findings.push(`${label} describes a different package digest`);
|
||||
if (assembler?.openrigVersion !== cfg.assembler.openrigVersion) {
|
||||
findings.push(`${label} was generated by OpenRig ${assembler?.openrigVersion}, not ${cfg.assembler.openrigVersion}`);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function checkStatus(root, validators) {
|
||||
const file = path.join(root, "status", "status.json");
|
||||
if (!fs.existsSync(file)) return [];
|
||||
let status;
|
||||
try {
|
||||
status = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (error) {
|
||||
return [`status/status.json: not readable JSON: ${error.message}`];
|
||||
}
|
||||
const findings = [];
|
||||
if (!validators.status(status)) {
|
||||
findings.push(`status/status.json: not bundle-status v1: ${schemaError(validators.status)}`);
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(GENERATED)) {
|
||||
if (status.generated[key] !== value) findings.push(`status/status.json: generated.${key} must be ${JSON.stringify(value)}`);
|
||||
}
|
||||
if (status.bodyDigest !== statusBodyDigest(status)) {
|
||||
findings.push("status/status.json: bodyDigest does not match the content, so it was edited by hand; regenerate it with tools/status.mjs");
|
||||
}
|
||||
}
|
||||
for (const leak of findPrivateText(status)) findings.push(`status/status.json: private text (${leak.kind}) at ${leak.at}`);
|
||||
for (const at of findIllFormedText(status)) findings.push(`status/status.json: text that isn't well-formed Unicode at ${at}`);
|
||||
return findings;
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
let root = REPO;
|
||||
if (argv[0] === "--root" && argv[1]) root = path.resolve(argv[1]);
|
||||
else if (argv.length) {
|
||||
console.error("usage: node tools/registry-check.mjs [--root <dir>]");
|
||||
return 2;
|
||||
}
|
||||
const findings = checkRepository(root);
|
||||
for (const { file, message } of submissionNotices(root)) {
|
||||
console.log(`${file}: ${message}`);
|
||||
// Shown on the pull request itself, so a submitter needn't open the log.
|
||||
if (process.env.GITHUB_ACTIONS === "true") console.log(`::notice file=${file},title=Submission received::${message}`);
|
||||
}
|
||||
for (const finding of findings) console.error(finding);
|
||||
if (findings.length) {
|
||||
console.error(`registry check failed: ${findings.length} finding(s)`);
|
||||
return 1;
|
||||
}
|
||||
console.log("registry check passed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
process.exitCode = main(process.argv.slice(2));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/journey.v1.json",
|
||||
"title": "Journey v1 (openrig-world/status/journeys/<id>-v<version>.json)",
|
||||
"description": "The required steps a run must PASS for a label. A run record names its journey by id and version.",
|
||||
"type": "object",
|
||||
"required": ["schema", "id", "version", "kind", "title", "requiredSteps"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema": { "const": "openrig.journey/v1" },
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"kind": { "enum": ["team", "harness_check"] },
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"requiredSteps": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "description"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
||||
"description": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/bundle-behaviour.v1.json",
|
||||
"title": "Before-install behaviour view (openrig.bundle-behaviour/v1)",
|
||||
"description": "Derived from the archive's files alone, for one configuration and one assembler version; kept outside the archive, so it never feeds the package digest. A view, never a gate; carries no tested status. Consumers ignore unknown properties; producers add only optional properties within v1. An empty list means none are known; something unknown goes in unknownBeforeLaunch.",
|
||||
"$defs": {
|
||||
"sourceRef": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Archive-relative file, and optionally the field within it."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"state",
|
||||
"identity"
|
||||
],
|
||||
"properties": {
|
||||
"schema": {
|
||||
"const": "openrig.bundle-behaviour/v1"
|
||||
},
|
||||
"state": {
|
||||
"enum": [
|
||||
"generated",
|
||||
"not_generated"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"localInspectCommand": {
|
||||
"type": "string",
|
||||
"description": "For not_generated: the existing command that shows this view locally, for example rig bundle inspect <archive> --json."
|
||||
},
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"configurationId",
|
||||
"packageDigest",
|
||||
"assembler",
|
||||
"generator"
|
||||
],
|
||||
"properties": {
|
||||
"source": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/source"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurationId": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/configurationId"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "null when the archive doesn't state it (a local or legacy archive)."
|
||||
},
|
||||
"packageDigest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/packageDigest"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assembler": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/assembler"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "The OpenRig that built the archive, as the archive states it; null when unstated. Never the inspecting OpenRig."
|
||||
},
|
||||
"generator": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/assembler",
|
||||
"description": "The OpenRig that produced this view."
|
||||
},
|
||||
"compatibility": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minDaemonVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"minCliVersion": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"statedProvenance": {
|
||||
"type": "object",
|
||||
"description": "As the bundle states it; not verified."
|
||||
},
|
||||
"integrity": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"digestValid",
|
||||
"filesVerified"
|
||||
],
|
||||
"properties": {
|
||||
"digestValid": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"filesVerified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"description": "Self-consistency of the archive; not authorship."
|
||||
}
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"seat",
|
||||
"pod",
|
||||
"member",
|
||||
"agentRef",
|
||||
"runtime",
|
||||
"cwd"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"pod": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"member": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"agentRef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"profile": {
|
||||
"type": "string"
|
||||
},
|
||||
"runtime": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/runtime"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"posture": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"seat",
|
||||
"shellAccess",
|
||||
"selection",
|
||||
"basis",
|
||||
"nativeEffect"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"shellAccess": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"selection": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"basis": {
|
||||
"enum": [
|
||||
"explicit",
|
||||
"product_default",
|
||||
"unresolved"
|
||||
],
|
||||
"description": "Declared in the archive, the product's conditional default, or not resolvable before launch."
|
||||
},
|
||||
"nativeEffect": {
|
||||
"const": "unknown",
|
||||
"description": "Host settings (for example OPENRIG_YOLO, persisted or native settings) decide the effective permission at launch."
|
||||
},
|
||||
"permissionPrompts": {
|
||||
"enum": ["off", "on", "default"],
|
||||
"description": "Archive declaration per seat: off for full_bypass (including builtin:yolo) on any runtime; on for a Claude/Codex flag-surface floor policy, or Codex under auto; default for Claude/Codex with no declared policy. Omitted for Claude under auto, config-surface policies, host-resolved Codex profiles, and Pi except declared full bypass. Absent means not stated, never on. Host settings may differ; nativeEffect remains unknown."
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
},
|
||||
"firstRunWarnings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"claudeBypass": {
|
||||
"const": "harness_asks_once"
|
||||
}
|
||||
},
|
||||
"description": "Declared full-bypass Claude seat: the harness asks for bypass-warning acceptance if it has not been remembered. No host acceptance state is read; nativeEffect stays unknown."
|
||||
},
|
||||
"nonInterruptive": {
|
||||
"const": "available",
|
||||
"description": "This declared full-bypass Claude/Codex seat can use --non-interruptive launch flags. Availability is not the operator choice or proof that no prompt will appear. Omitted for other postures, unresolved policies and runtimes with no such flags."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"toldFiles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"pathOrRef",
|
||||
"resolution"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"pathOrRef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"delivery": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolution": {
|
||||
"enum": [
|
||||
"archive",
|
||||
"host_at_launch",
|
||||
"unresolved"
|
||||
]
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"alsoRuns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"pathOrRef",
|
||||
"resolution"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"pathOrRef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolution": {
|
||||
"enum": [
|
||||
"archive",
|
||||
"host_at_launch",
|
||||
"unresolved"
|
||||
]
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"writes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"destinationBase",
|
||||
"path",
|
||||
"operation",
|
||||
"phase"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"destinationBase": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outsideAddresses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"domain",
|
||||
"sourceRefs"
|
||||
],
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"needs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"name",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"versionConstraint": {
|
||||
"type": "string"
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"description": "Unexecuted author setup lines, in order in one shell; the install command follows in the resulting working directory.",
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"status": {
|
||||
"const": "not_checked"
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"unknownBeforeLaunch": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"subject",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"seat": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/member"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourceRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/sourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"state": {
|
||||
"const": "not_generated"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"reason",
|
||||
"localInspectCommand"
|
||||
],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"team"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"posture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"toldFiles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"alsoRuns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"writes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"outsideAddresses"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"needs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"unknownBeforeLaunch"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"state": {
|
||||
"const": "generated"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"team",
|
||||
"posture",
|
||||
"toldFiles",
|
||||
"alsoRuns",
|
||||
"writes",
|
||||
"outsideAddresses",
|
||||
"needs",
|
||||
"unknownBeforeLaunch"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/bundle-common.v1.json",
|
||||
"title": "Shared bundle identity definitions (v1)",
|
||||
"description": "Source identity, configuration ID, package digest and assembler, shared by every bundle format. See docs/reference/bundle-formats.md.",
|
||||
"$defs": {
|
||||
"source": {
|
||||
"type": "object",
|
||||
"description": "Source identity: repository + resolvedCommit + folder. requestedRef and canonicalUrl explain it and are not part of it.",
|
||||
"required": [
|
||||
"repository",
|
||||
"folder",
|
||||
"resolvedCommit"
|
||||
],
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"pattern": "^https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"
|
||||
},
|
||||
"folder": {
|
||||
"type": "string",
|
||||
"pattern": "^(\\.|(?!/)(?!.*(^|/)\\.\\.(/|$))[^\\\\]+)$",
|
||||
"description": "Repo-relative POSIX path of the folder holding rig.yaml; '.' is the repository root."
|
||||
},
|
||||
"resolvedCommit": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
},
|
||||
"requestedRef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"canonicalUrl": {
|
||||
"type": "string",
|
||||
"pattern": "^https://github\\.com/.+/tree/[0-9a-f]{40}(/.*)?$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"member": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
|
||||
"description": "pod.member"
|
||||
},
|
||||
"runtime": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
||||
"description": "A runtime as rig.yaml spells it, for example claude-code, codex, pi."
|
||||
},
|
||||
"configurationId": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+=[a-z0-9][a-z0-9-]*(,[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+=[a-z0-9][a-z0-9-]*)*$",
|
||||
"description": "Every member's pod.member=runtime, sorted by pod.member in code-unit order, joined with ','."
|
||||
},
|
||||
"packageDigest": {
|
||||
"type": "object",
|
||||
"description": "SHA-256 over UTF-8 lines JSON.stringify(path) + '\\t' + sha256 + '\\n', one per integrity.files entry, sorted by path in code-unit order. See docs/reference/bundle-formats.md.",
|
||||
"required": [
|
||||
"algorithm",
|
||||
"value",
|
||||
"coverage"
|
||||
],
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"const": "sha256"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"coverage": {
|
||||
"const": "openrig.package-digest/v1"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"assembler": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"openrigVersion"
|
||||
],
|
||||
"properties": {
|
||||
"openrigVersion": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{7,40}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sha256": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"algorithm",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"const": "sha256"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/bundle-status.v1.json",
|
||||
"title": "Public status file (generated; do not edit)",
|
||||
"description": "Built from run records by rule openrig.status-rule/v1, from an allowlist of output fields: no private paths, host names, row IDs, account names or receipt text. Regenerating gives identical bytes. An empty platforms object means Not tested. Closed on purpose: no property outside this schema may appear, so a private field can't leak in.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"generated",
|
||||
"listings",
|
||||
"harnessChecks",
|
||||
"bodyDigest"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"generated": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"by",
|
||||
"notice",
|
||||
"rule",
|
||||
"generatorVersion"
|
||||
],
|
||||
"properties": {
|
||||
"by": {
|
||||
"type": "string"
|
||||
},
|
||||
"notice": {
|
||||
"const": "Generated file. Do not edit by hand; regenerate it."
|
||||
},
|
||||
"rule": {
|
||||
"const": "openrig.status-rule/v1"
|
||||
},
|
||||
"generatorVersion": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"listings": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state",
|
||||
"configurations"
|
||||
],
|
||||
"properties": {
|
||||
"state": {
|
||||
"enum": [
|
||||
"ok",
|
||||
"status_unavailable"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/configurationId"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"platforms",
|
||||
"communityReports"
|
||||
],
|
||||
"properties": {
|
||||
"platforms": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"label",
|
||||
"date",
|
||||
"openrigVersion",
|
||||
"recordIds",
|
||||
"packageDigests"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"enum": [
|
||||
"known_problem",
|
||||
"tested",
|
||||
"tested_with_help",
|
||||
"partly_tested",
|
||||
"not_tested",
|
||||
"status_unavailable"
|
||||
]
|
||||
},
|
||||
"assistanceCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"openrigVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"recordIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
||||
}
|
||||
},
|
||||
"packageDigests": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/packageDigest"
|
||||
}
|
||||
},
|
||||
"note": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"label": {
|
||||
"enum": [
|
||||
"tested",
|
||||
"tested_with_help",
|
||||
"partly_tested",
|
||||
"known_problem"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"recordIds": {
|
||||
"minItems": 1
|
||||
},
|
||||
"packageDigests": {
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"label": {
|
||||
"const": "tested_with_help"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"assistanceCount"
|
||||
],
|
||||
"properties": {
|
||||
"assistanceCount": {
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"label": {
|
||||
"const": "tested"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"assistanceCount": {
|
||||
"const": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(linux|darwin|win32)-(x64|arm64)$"
|
||||
}
|
||||
},
|
||||
"communityReports": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"state": {
|
||||
"const": "status_unavailable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"additionalProperties": {
|
||||
"properties": {
|
||||
"platforms": {
|
||||
"additionalProperties": {
|
||||
"properties": {
|
||||
"label": {
|
||||
"const": "status_unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"harnessChecks": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state",
|
||||
"harnesses"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"state": {
|
||||
"enum": [
|
||||
"ok",
|
||||
"status_unavailable"
|
||||
],
|
||||
"description": "status_unavailable: some harness-check record could not be read."
|
||||
},
|
||||
"harnesses": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/runtime"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"result",
|
||||
"date",
|
||||
"openrigVersion",
|
||||
"harnessVersion",
|
||||
"recordIds"
|
||||
],
|
||||
"properties": {
|
||||
"result": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL",
|
||||
"BLOCKED",
|
||||
"NOT_RUN"
|
||||
]
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"openrigVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"harnessVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"recordIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
||||
}
|
||||
},
|
||||
"note": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(linux|darwin|win32)-(x64|arm64)$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"state": {
|
||||
"const": "status_unavailable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"harnesses": {
|
||||
"maxProperties": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"bodyDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$",
|
||||
"description": "SHA-256, lowercase hex, over the RFC 8785 (JCS) canonical JSON, as UTF-8, of an object holding every other top-level field."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/registry-entry.v1.json",
|
||||
"title": "Registry entry (openrig-world/registry/<slug>.yaml)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"slug",
|
||||
"name",
|
||||
"source",
|
||||
"configurations",
|
||||
"review",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"schema": {
|
||||
"const": "openrig.registry-entry/v1"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"summary": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/source"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"packageDigest",
|
||||
"assembler",
|
||||
"behaviour"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/configurationId"
|
||||
},
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
||||
},
|
||||
"recommended": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"packageDigest": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/packageDigest"
|
||||
},
|
||||
"assembler": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/assembler"
|
||||
},
|
||||
"behaviour": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$)).+\\.json$",
|
||||
"description": "Registry-relative path of the generated behaviour view for this configuration and assembler version."
|
||||
},
|
||||
"evidenceReuse": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"packageDigest",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"packageDigest": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/packageDigest"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Earlier packages whose evidence still applies to this one, with the public reason (for example 'only EVIDENCE.md changed'). Labels resting on it show both digests."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"date"
|
||||
],
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"note": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"listed",
|
||||
"withdrawn"
|
||||
]
|
||||
},
|
||||
"withdrawnOn": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"description": "When the entry was withdrawn; set with status: withdrawn."
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"const": "withdrawn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"withdrawnOn"
|
||||
]
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"required": [
|
||||
"withdrawnOn"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/run-record.v1.json",
|
||||
"title": "Run record v1 (private; kept beside its receipt)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"id",
|
||||
"subject",
|
||||
"execution",
|
||||
"environment",
|
||||
"evidence",
|
||||
"journey",
|
||||
"outcome",
|
||||
"relations"
|
||||
],
|
||||
"properties": {
|
||||
"schema": {
|
||||
"const": "openrig.run-record/v1"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
||||
},
|
||||
"subject": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"source",
|
||||
"configurationId",
|
||||
"packageDigest",
|
||||
"assembler"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "team"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/source"
|
||||
},
|
||||
"configurationId": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/configurationId"
|
||||
},
|
||||
"packageDigest": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/packageDigest"
|
||||
},
|
||||
"assembler": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/assembler"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"harness"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "harness_check"
|
||||
},
|
||||
"harness": {
|
||||
"$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/runtime"
|
||||
}
|
||||
},
|
||||
"description": "A harness check never upgrades a team label.",
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"execution": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"resources",
|
||||
"settings",
|
||||
"unknowns"
|
||||
],
|
||||
"properties": {
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"type": "object"
|
||||
},
|
||||
"unknowns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"openrig",
|
||||
"platform",
|
||||
"arch",
|
||||
"harnesses"
|
||||
],
|
||||
"properties": {
|
||||
"openrig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"commit": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"platform": {
|
||||
"enum": [
|
||||
"linux",
|
||||
"darwin",
|
||||
"win32"
|
||||
],
|
||||
"description": "Node's process.platform; with arch, it forms the status file's <os>-<arch> platform key."
|
||||
},
|
||||
"arch": {
|
||||
"enum": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"description": "Node's process.arch."
|
||||
},
|
||||
"harnesses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"knownBy"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"knownBy": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"receipt"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"native-workflow",
|
||||
"daemon-adoption",
|
||||
"running-seat",
|
||||
"installed-only",
|
||||
"synthetic",
|
||||
"community-reported"
|
||||
]
|
||||
},
|
||||
"receipt": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ref",
|
||||
"sha256"
|
||||
],
|
||||
"properties": {
|
||||
"ref": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"journey": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"outcome": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"steps",
|
||||
"assistance",
|
||||
"recordedBy",
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"result"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"result": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL",
|
||||
"BLOCKED",
|
||||
"NOT_RUN"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistance": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"count"
|
||||
],
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedBy": {
|
||||
"type": "string"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"publicNote": {
|
||||
"type": "string",
|
||||
"maxLength": 280,
|
||||
"description": "A short public cause for a BLOCKED or FAIL result; the only record text that reaches status.json."
|
||||
}
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"supersedes",
|
||||
"withdraws",
|
||||
"resolves"
|
||||
],
|
||||
"properties": {
|
||||
"supersedes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"withdraws": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resolves": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"record",
|
||||
"step"
|
||||
],
|
||||
"properties": {
|
||||
"record": {
|
||||
"type": "string"
|
||||
},
|
||||
"step": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"repository": "https://github.com/mvschwarz/openrig",
|
||||
"path": "docs/reference/schemas",
|
||||
"commit": "6f783f67e1927190c3513c1ff53083d9ab61355f",
|
||||
"files": {
|
||||
"bundle-common.v1.schema.json": "945d743ec36292dc90210bd3dd2bbd36eb98861b9179cd8d114d8c5aabdfc1eb",
|
||||
"bundle-behaviour.v1.schema.json": "36fe07246c9f2c8648696603d7c51f901a98a902a2baeb4084901e2120e43d0d",
|
||||
"run-record.v1.schema.json": "b3c7ac64fd9dd51152888c99fdbcadd85cddebebb2d38253864bff054da3fde7",
|
||||
"bundle-status.v1.schema.json": "df2e6be1d69a2b741804ad46edb8557a136afeb098e00a2d3d33237f5198ece5",
|
||||
"registry-entry.v1.schema.json": "9a0326daa76169cad427fa6ba75a21a8e010bf6239ca5fcbc5c4ea80f242c7f5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openrig.dev/schemas/registry-submission.v1.json",
|
||||
"title": "Registry submission v1 (openrig-world/registry/submissions/<name>.yaml)",
|
||||
"description": "A rig bundle submitted for listing: where it lives, nothing else. A maintainer completes the registry entry (pinned commit, configurations, package digests, behaviour views, review). A submission is never listed.",
|
||||
"type": "object",
|
||||
"required": ["repository", "folder", "ref"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"repository": { "$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/source/properties/repository" },
|
||||
"folder": { "$ref": "https://openrig.dev/schemas/bundle-common.v1.json#/$defs/source/properties/folder" },
|
||||
"ref": { "type": "string", "minLength": 1, "description": "A branch, tag or commit." }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates status/status.json from private run records (rule openrig.status-rule/v1), or checks that the
|
||||
// committed file equals regenerated output. Run it where the records and their receipts are.
|
||||
//
|
||||
// node tools/status.mjs generate --records <dir> [--records <dir> ...] [--root <registry-root>] [--out <file>]
|
||||
// node tools/status.mjs check --records <dir> [--records <dir> ...] [--root <registry-root>] [--out <file>]
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { canonicalFile } from "./lib/canonical.mjs";
|
||||
import { loadJourneys, loadRecords, loadRegistry, loadValidators, schemaError } from "./lib/load.mjs";
|
||||
import { findIllFormedText, findPrivateText } from "./lib/private-patterns.mjs";
|
||||
import { deriveStatus } from "./lib/status-rule.mjs";
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
/** Everything `generate` and `check` need; `problems` non-empty means nothing may be written. */
|
||||
export function generate({ root = REPO, recordRoots }) {
|
||||
const validators = loadValidators();
|
||||
const problems = [...validators.problems];
|
||||
const registry = loadRegistry(root, validators);
|
||||
for (const item of registry) if (item.problem) problems.push(`${item.file}: ${item.problem}`);
|
||||
const { journeys, problems: journeyProblems } = loadJourneys(root, validators);
|
||||
problems.push(...journeyProblems);
|
||||
if (problems.length) return { problems, unavailable: [], warnings: [] };
|
||||
|
||||
const entries = registry.map((item) => item.entry);
|
||||
const { teamFiles, harnessFiles, warnings } = loadRecords(recordRoots, validators);
|
||||
const listed = new Set(entries.filter((e) => e.status === "listed").map((e) => e.slug));
|
||||
for (const slug of teamFiles.keys()) if (!listed.has(slug)) warnings.push(`records for ${slug}, which has no listed registry entry, were not used`);
|
||||
|
||||
const { status, problems: unavailable } = deriveStatus({ entries, teamFiles, harnessFiles, journeys });
|
||||
if (!validators.status(status)) problems.push(`the generated status is not bundle-status v1: ${schemaError(validators.status)}`);
|
||||
for (const leak of findPrivateText(status)) problems.push(`the generated status carries private text (${leak.kind}) at ${leak.at}`);
|
||||
for (const at of findIllFormedText(status)) problems.push(`the generated status carries text that isn't well-formed Unicode at ${at}`);
|
||||
return { status, text: canonicalFile(status), problems, unavailable, warnings };
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const [command, ...rest] = argv;
|
||||
const recordRoots = [];
|
||||
let root = REPO;
|
||||
let out = null;
|
||||
for (let i = 0; i < rest.length; i++) {
|
||||
const value = rest[i + 1];
|
||||
if (rest[i] === "--records" && value) recordRoots.push(path.resolve(value));
|
||||
else if (rest[i] === "--root" && value) root = path.resolve(value);
|
||||
else if (rest[i] === "--out" && value) out = path.resolve(value);
|
||||
else return usage(`unknown or incomplete argument: ${rest[i]}`);
|
||||
i++;
|
||||
}
|
||||
if (!["generate", "check"].includes(command) || recordRoots.length === 0) return usage();
|
||||
out ??= path.join(root, "status", "status.json");
|
||||
|
||||
const result = generate({ root, recordRoots });
|
||||
for (const warning of result.warnings) console.error(`warning: ${warning}`);
|
||||
for (const reason of result.unavailable) console.error(`status unavailable: ${reason}`);
|
||||
if (result.problems.length) {
|
||||
for (const problem of result.problems) console.error(`error: ${problem}`);
|
||||
console.error("nothing was written");
|
||||
return 1;
|
||||
}
|
||||
if (command === "generate") {
|
||||
fs.mkdirSync(path.dirname(out), { recursive: true });
|
||||
fs.writeFileSync(out, result.text);
|
||||
console.log(`wrote ${out} (bodyDigest ${result.status.bodyDigest})`);
|
||||
return 0;
|
||||
}
|
||||
const committed = fs.existsSync(out) ? fs.readFileSync(out, "utf8") : null;
|
||||
if (committed === result.text) {
|
||||
console.log(`${out} equals regenerated output`);
|
||||
return 0;
|
||||
}
|
||||
console.error(committed === null ? `${out} does not exist` : `${out} differs from regenerated output; run generate`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
function usage(reason) {
|
||||
if (reason) console.error(reason);
|
||||
console.error("usage: node tools/status.mjs generate|check --records <dir> [--records <dir> ...] [--root <dir>] [--out <file>]");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
process.exitCode = main(process.argv.slice(2));
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"schema": "openrig.bundle-behaviour/v1",
|
||||
"state": "generated",
|
||||
"identity": {
|
||||
"source": {
|
||||
"repository": "https://github.com/mvschwarz/openrig-world",
|
||||
"folder": "rigs/openrig-dev",
|
||||
"resolvedCommit": "894ad0e301ab46ac6ddd74dea784b7f6311d69a2",
|
||||
"requestedRef": "main",
|
||||
"canonicalUrl": "https://github.com/mvschwarz/openrig-world/tree/894ad0e301ab46ac6ddd74dea784b7f6311d69a2/rigs/openrig-dev"
|
||||
},
|
||||
"configurationId": "build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex",
|
||||
"packageDigest": {
|
||||
"algorithm": "sha256",
|
||||
"value": "5f2c0a1e9b8d7c6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a29",
|
||||
"coverage": "openrig.package-digest/v1"
|
||||
},
|
||||
"assembler": {
|
||||
"openrigVersion": "0.6.6",
|
||||
"commit": "910bf9f1"
|
||||
},
|
||||
"generator": {
|
||||
"openrigVersion": "0.6.6",
|
||||
"commit": "910bf9f1"
|
||||
},
|
||||
"integrity": {
|
||||
"digestValid": true,
|
||||
"filesVerified": true
|
||||
},
|
||||
"statedProvenance": {}
|
||||
},
|
||||
"team": [
|
||||
{
|
||||
"seat": "build.lead",
|
||||
"pod": "build",
|
||||
"member": "lead",
|
||||
"agentRef": "local:agents/openrig-dev",
|
||||
"profile": "lead",
|
||||
"runtime": "claude-code",
|
||||
"cwd": "--cwd (the user's openrig clone)"
|
||||
},
|
||||
{
|
||||
"seat": "build.impl",
|
||||
"pod": "build",
|
||||
"member": "impl",
|
||||
"agentRef": "local:agents/openrig-dev",
|
||||
"profile": "implementer",
|
||||
"runtime": "claude-code",
|
||||
"cwd": "--cwd (the user's openrig clone)"
|
||||
},
|
||||
{
|
||||
"seat": "check.review",
|
||||
"pod": "check",
|
||||
"member": "review",
|
||||
"agentRef": "local:agents/openrig-dev",
|
||||
"profile": "reviewer",
|
||||
"runtime": "codex",
|
||||
"cwd": "--cwd (the user's openrig clone)"
|
||||
},
|
||||
{
|
||||
"seat": "check.qa",
|
||||
"pod": "check",
|
||||
"member": "qa",
|
||||
"agentRef": "local:agents/openrig-dev",
|
||||
"profile": "qa",
|
||||
"runtime": "codex",
|
||||
"cwd": "--cwd (the user's openrig clone)"
|
||||
}
|
||||
],
|
||||
"posture": [
|
||||
{
|
||||
"seat": "build.lead",
|
||||
"shellAccess": "runs shell commands in its working folder under your logged-in account",
|
||||
"selection": "none declared",
|
||||
"basis": "product_default",
|
||||
"nativeEffect": "unknown",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"seat": "build.impl",
|
||||
"shellAccess": "runs shell commands in its working folder under your logged-in account",
|
||||
"selection": "none declared",
|
||||
"basis": "product_default",
|
||||
"nativeEffect": "unknown",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"seat": "check.review",
|
||||
"shellAccess": "runs shell commands in its working folder under your logged-in account",
|
||||
"selection": "none declared",
|
||||
"basis": "product_default",
|
||||
"nativeEffect": "unknown",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"seat": "check.qa",
|
||||
"shellAccess": "runs shell commands in its working folder under your logged-in account",
|
||||
"selection": "none declared",
|
||||
"basis": "product_default",
|
||||
"nativeEffect": "unknown",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"toldFiles": [
|
||||
{
|
||||
"kind": "culture",
|
||||
"pathOrRef": "CULTURE.md",
|
||||
"delivery": "guidance_merge",
|
||||
"resolution": "archive",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml",
|
||||
"field": "culture_file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "startup",
|
||||
"pathOrRef": "startup/context.md",
|
||||
"delivery": "send_text",
|
||||
"resolution": "archive",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml",
|
||||
"field": "startup.files"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"seat": "build.lead",
|
||||
"kind": "role",
|
||||
"pathOrRef": "agents/openrig-dev/guidance/lead.md",
|
||||
"resolution": "archive",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "agents/openrig-dev/agent.yaml",
|
||||
"field": "profiles.lead"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"alsoRuns": [
|
||||
{
|
||||
"kind": "plugin",
|
||||
"pathOrRef": "openrig-home:plugins/openrig-core",
|
||||
"resolution": "host_at_launch",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "agents/openrig-dev/agent.yaml",
|
||||
"field": "resources.plugins"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "hook",
|
||||
"pathOrRef": "agents/openrig-dev/runtime/claude-activity-hooks.json",
|
||||
"trigger": "Claude Code activity events",
|
||||
"resolution": "archive",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "agents/openrig-dev/agent.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
{
|
||||
"kind": "managed_block",
|
||||
"destinationBase": "each seat's working folder",
|
||||
"path": "CLAUDE.local.md",
|
||||
"operation": "add OpenRig block",
|
||||
"phase": "launch",
|
||||
"condition": "Claude Code seats",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml",
|
||||
"field": "managed_blocks"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "managed_block",
|
||||
"destinationBase": "each seat's working folder",
|
||||
"path": "AGENTS.md",
|
||||
"operation": "add OpenRig block",
|
||||
"phase": "launch",
|
||||
"condition": "Codex seats"
|
||||
}
|
||||
],
|
||||
"outsideAddresses": [
|
||||
{
|
||||
"domain": "github.com",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "README.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"needs": [
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "claude-code",
|
||||
"status": "not_checked",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "codex",
|
||||
"status": "not_checked",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "rig.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"name": "GitHub account with push credentials",
|
||||
"status": "not_checked",
|
||||
"sourceRefs": [
|
||||
{
|
||||
"path": "README.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"unknownBeforeLaunch": [
|
||||
{
|
||||
"subject": "effective permissions",
|
||||
"reason": "host settings decide them at launch"
|
||||
},
|
||||
{
|
||||
"subject": "model and effort",
|
||||
"reason": "not declared; the runtime's defaults apply"
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
schema: openrig.registry-entry/v1
|
||||
slug: openrig-dev
|
||||
name: OpenRig contributor team
|
||||
summary: A four-seat team that turns one OpenRig issue into one pull request.
|
||||
source:
|
||||
repository: https://github.com/mvschwarz/openrig-world
|
||||
folder: rigs/openrig-dev
|
||||
resolvedCommit: 894ad0e
|
||||
requestedRef: main
|
||||
configurations:
|
||||
- id: build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex
|
||||
alias: recommended
|
||||
recommended: true
|
||||
packageDigest: { algorithm: sha256, value: 5f2c0a1e9b8d7c6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a29, coverage: openrig.package-digest/v1 }
|
||||
assembler: { openrigVersion: 0.6.6, commit: 910bf9f1 }
|
||||
behaviour: views/openrig-dev/build.impl-claude-code_build.lead-claude-code_check.qa-codex_check.review-codex.0.6.6.json
|
||||
review:
|
||||
date: 2026-10-04
|
||||
note: Reviewed for listing. Review is not a security audit.
|
||||
status: listed
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
schema: openrig.registry-entry/v1
|
||||
slug: openrig-dev
|
||||
name: OpenRig contributor team
|
||||
summary: A four-seat team that turns one OpenRig issue into one pull request.
|
||||
source:
|
||||
repository: https://github.com/mvschwarz/openrig-world
|
||||
folder: rigs/openrig-dev
|
||||
resolvedCommit: 894ad0e301ab46ac6ddd74dea784b7f6311d69a2
|
||||
requestedRef: main
|
||||
configurations:
|
||||
- id: build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex
|
||||
alias: recommended
|
||||
recommended: true
|
||||
packageDigest: { algorithm: sha256, value: 5f2c0a1e9b8d7c6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a29, coverage: openrig.package-digest/v1 }
|
||||
assembler: { openrigVersion: 0.6.6, commit: 910bf9f1 }
|
||||
behaviour: views/openrig-dev/build.impl-claude-code_build.lead-claude-code_check.qa-codex_check.review-codex.0.6.6.json
|
||||
review:
|
||||
date: 2026-10-04
|
||||
note: Reviewed for listing. Review is not a security audit.
|
||||
status: listed
|
||||
Vendored
+346
@@ -0,0 +1,346 @@
|
||||
// Status rule fixtures: each case is a registry entry, some run records (each in its own run folder with its
|
||||
// receipt), and the labels the rule must produce. The test writes them to a temporary folder and runs the
|
||||
// real generator over it.
|
||||
export const DIGEST_A = { algorithm: "sha256", value: "a".repeat(64), coverage: "openrig.package-digest/v1" };
|
||||
export const DIGEST_B = { algorithm: "sha256", value: "b".repeat(64), coverage: "openrig.package-digest/v1" };
|
||||
const SOURCE = { repository: "https://github.com/example/rigs", folder: "team", resolvedCommit: "1".repeat(40) };
|
||||
export const MIX = "dev.impl=claude-code,dev.review=codex";
|
||||
const STEPS = ["install", "launch", "orient", "pull-request", "merged"];
|
||||
const ALL_PASS = STEPS.map((id) => [id, "PASS"]);
|
||||
|
||||
export function entry({ digest = DIGEST_A, openrig = "0.6.6", evidenceReuse } = {}) {
|
||||
const cfg = { id: MIX, alias: "recommended", recommended: true, packageDigest: digest, assembler: { openrigVersion: openrig }, behaviour: "views/team/recommended.json" };
|
||||
if (evidenceReuse) cfg.evidenceReuse = evidenceReuse;
|
||||
return { schema: "openrig.registry-entry/v1", slug: "team", name: "Example team", source: SOURCE, configurations: [cfg], review: { date: "2026-10-04" }, status: "listed" };
|
||||
}
|
||||
|
||||
/** A team run record; `receipt.sha256` is filled in by the test from the receipt it writes. */
|
||||
export function team(id, { steps = ALL_PASS, assistance = 0, platform = "linux", openrig = "0.6.6", digest = DIGEST_A, kind = "native-workflow", at = "2026-10-04T10:00:00Z", relations = {}, publicNote, recordedBy = "fleet", assistanceText = "a person typed one command for the seat", configurationId = MIX } = {}) {
|
||||
const outcome = { steps: steps.map(([step, result]) => ({ id: step, result })), assistance: { count: assistance }, recordedBy, at };
|
||||
if (assistance) outcome.assistance.description = assistanceText;
|
||||
if (publicNote) outcome.publicNote = publicNote;
|
||||
return {
|
||||
schema: "openrig.run-record/v1", id,
|
||||
subject: { kind: "team", source: SOURCE, configurationId, packageDigest: digest, assembler: { openrigVersion: openrig } },
|
||||
execution: { resources: [], settings: {}, unknowns: [] },
|
||||
environment: { openrig: { version: openrig }, platform, arch: "x64", harnesses: [{ name: "claude-code", version: "2.1.220", knownBy: "claude --version" }] },
|
||||
evidence: { kind, receipt: { ref: "receipt.md", sha256: "" } },
|
||||
journey: { id: "contributor-team", version: 1 },
|
||||
outcome,
|
||||
relations: { supersedes: [], withdraws: [], resolves: [], ...relations },
|
||||
};
|
||||
}
|
||||
|
||||
export function harness(id, { harnessName = "pi", steps = [["on-path", "PASS"], ["version-supported", "PASS"], ["signed-in", "BLOCKED"]], publicNote, at = "2026-10-04T11:00:00Z" } = {}) {
|
||||
const record = team(id, { at, publicNote });
|
||||
record.subject = { kind: "harness_check", harness: harnessName };
|
||||
record.journey = { id: "harness-check", version: 1 };
|
||||
record.environment.harnesses = [{ name: harnessName, version: "0.9.1", knownBy: "pi --version" }];
|
||||
record.outcome.steps = steps.map(([step, result]) => ({ id: step, result }));
|
||||
return record;
|
||||
}
|
||||
|
||||
const ok = (configuration) => ({ state: "ok", configurations: { [MIX]: configuration } });
|
||||
const FAIL_MERGED = STEPS.map((id) => [id, id === "merged" ? "FAIL" : "PASS"]);
|
||||
|
||||
// `files`: { group, record } | { group, raw } with optional `receipt` ("missing" or "changed").
|
||||
export const CASES = [
|
||||
{
|
||||
name: "a plain pass is Tested by OpenRig",
|
||||
files: [{ group: "team", record: team("r1") }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r1"], openrigVersion: "0.6.6" } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "an assisted pass is Tested with help (N)",
|
||||
files: [{ group: "team", record: team("r1", { assistance: 2 }) }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested_with_help", assistanceCount: 2 } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a partial run is Partly tested",
|
||||
files: [{ group: "team", record: team("r1", { steps: [["install", "PASS"], ["launch", "PASS"], ["orient", "BLOCKED"]] }) }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "partly_tested" } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a readable PASS whose receipt was deleted makes the listing Status unavailable, not Not tested",
|
||||
files: [{ group: "team", record: team("r1"), receipt: "missing" }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a PASS whose receipt changed after the record makes the listing Status unavailable",
|
||||
files: [{ group: "team", record: team("r1"), receipt: "changed" }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a newer FAIL whose receipt is lost, beside an older PASS, makes the listing Status unavailable",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { steps: FAIL_MERGED, at: "2026-10-04T12:00:00Z" }), receipt: "missing" },
|
||||
],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a withdrawn record's receipt may be gone",
|
||||
files: [
|
||||
{ group: "team", record: team("r1"), receipt: "missing" },
|
||||
{ group: "team", record: team("r2", { steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r1"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a withdrawn record's withdrawal of a FAIL doesn't count",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED }) },
|
||||
{ group: "team", record: team("r2", { steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r1"] } }) },
|
||||
{ group: "team", record: team("r3", { steps: [["install", "NOT_RUN"]], at: "2026-10-04T13:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "withdrawals that form a cycle make the listing Status unavailable",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { relations: { withdraws: ["r2"] } }) },
|
||||
{ group: "team", record: team("r2", { at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r1"] } }) },
|
||||
],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a chain of corrections stays replaced",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { assistance: 1, at: "2026-10-04T12:00:00Z", relations: { supersedes: ["r1"] } }) },
|
||||
{ group: "team", record: team("r3", { assistance: 2, at: "2026-10-04T13:00:00Z", relations: { supersedes: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested_with_help", assistanceCount: 2, recordIds: ["r3"] } }, communityReports: 0 }),
|
||||
},
|
||||
// Round 3: a relation counts only from a record that could itself label the target's scope.
|
||||
...[["synthetic", 0], ["community-reported", 1]].map(([kind, reports]) => ({
|
||||
name: `a ${kind} record can't withdraw a native FAIL`,
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { steps: FAIL_MERGED, at: "2026-10-04T11:00:00Z" }) },
|
||||
{ group: "team", record: team("r3", { kind, steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r2"] } }, communityReports: reports }),
|
||||
})),
|
||||
{
|
||||
name: "a record for another configuration can't withdraw a FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { steps: FAIL_MERGED, at: "2026-10-04T11:00:00Z" }) },
|
||||
{ group: "team", record: team("r3", { configurationId: "dev.impl=codex,dev.review=codex", steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r2"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a record from another platform can't withdraw a FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { steps: FAIL_MERGED, at: "2026-10-04T11:00:00Z" }) },
|
||||
{ group: "team", record: team("r3", { platform: "darwin", steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r2"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a record for another configuration can't resolve a FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED }) },
|
||||
{ group: "team", record: team("r2", { configurationId: "dev.impl=codex,dev.review=codex", at: "2026-10-04T12:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a same-scope native withdrawal still clears a mistaken FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { steps: FAIL_MERGED, at: "2026-10-04T11:00:00Z" }) },
|
||||
{ group: "team", record: team("r3", { steps: [["install", "NOT_RUN"]], at: "2026-10-04T12:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a lost receipt on a record that can't label leaves the listing readable",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { kind: "synthetic" }), receipt: "missing" },
|
||||
{ group: "team", record: team("r3", { configurationId: "dev.impl=codex,dev.review=codex" }), receipt: "missing" },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a community report whose receipt is gone drops out of the count",
|
||||
files: [{ group: "team", record: team("r1", { kind: "community-reported" }), receipt: "missing" }],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "build metadata doesn't stop a same-version PASS resolving a FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED, openrig: "0.6.6+build1" }) },
|
||||
{ group: "team", record: team("r2", { openrig: "0.6.6+build2", at: "2026-10-04T12:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r2"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a newer run that needed help doesn't replace an older unassisted pass without supersedes",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { assistance: 1, at: "2026-10-04T12:00:00Z" }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a zero-assistance PASS corrected to assisted shows the correction",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { assistance: 1, at: "2026-10-04T12:00:00Z", relations: { supersedes: ["r1"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested_with_help", assistanceCount: 1, recordIds: ["r2"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "resolves from a BLOCKED record leaves the known problem",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED, publicNote: "The pull request was never merged." }) },
|
||||
{ group: "team", record: team("r2", { steps: [["merged", "BLOCKED"]], at: "2026-10-04T12:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r1"], note: "The pull request was never merged." } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "resolves from a withdrawn record leaves the known problem",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED }) },
|
||||
{ group: "team", record: team("r2", { at: "2026-10-04T12:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
{ group: "team", record: team("r3", { steps: [["install", "NOT_RUN"]], at: "2026-10-04T13:00:00Z", relations: { withdraws: ["r2"] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a PASS on a newer OpenRig that names the FAIL resolves it",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED }) },
|
||||
{ group: "team", record: team("r2", { openrig: "0.6.7", at: "2026-10-05T10:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", recordIds: ["r2"], openrigVersion: "0.6.7" } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a PASS on an older OpenRig doesn't resolve a FAIL",
|
||||
files: [
|
||||
{ group: "team", record: team("r1", { steps: FAIL_MERGED, openrig: "0.6.7" }) },
|
||||
{ group: "team", record: team("r2", { openrig: "0.6.6", at: "2026-10-05T10:00:00Z", relations: { resolves: [{ record: "r1", step: "merged" }] } }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "known_problem", recordIds: ["r1"] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "an unreadable record makes the listing Status unavailable, not Not tested",
|
||||
files: [{ group: "team", record: team("r1") }, { group: "team", raw: "{ this is not JSON" }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a record that isn't run record v1 makes the listing Status unavailable",
|
||||
files: [{ group: "team", raw: JSON.stringify({ schema: "openrig.run-record/v1", id: "r1" }) }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a record naming an unknown journey makes the listing Status unavailable",
|
||||
files: [{ group: "team", record: { ...team("r1"), journey: { id: "contributor-team", version: 9 } } }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a record from an environment the status format can't name makes the listing Status unavailable",
|
||||
files: [{ group: "team", record: { ...team("r1"), environment: { ...team("r1").environment, platform: "freebsd" } } }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a Linux PASS and a macOS FAIL are labelled per platform",
|
||||
files: [
|
||||
{ group: "team", record: team("r1") },
|
||||
{ group: "team", record: team("r2", { platform: "darwin", steps: FAIL_MERGED, publicNote: "Launch stopped on macOS." }) },
|
||||
],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested" }, "darwin-x64": { label: "known_problem", note: "Launch stopped on macOS." } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "an EVIDENCE.md-only change with evidenceReuse keeps the label and shows both packages",
|
||||
entry: entry({ digest: DIGEST_B, evidenceReuse: [{ packageDigest: DIGEST_A, reason: "Only EVIDENCE.md changed." }] }),
|
||||
files: [{ group: "team", record: team("r1", { digest: DIGEST_A }) }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", packageDigests: [DIGEST_B, DIGEST_A] } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "an agents/ change without evidenceReuse is Not tested",
|
||||
entry: entry({ digest: DIGEST_B }),
|
||||
files: [{ group: "team", record: team("r1", { digest: DIGEST_A }) }],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a newer OpenRig release keeps the label, dated with the version that ran",
|
||||
entry: entry({ openrig: "0.6.7" }),
|
||||
files: [{ group: "team", record: team("r1", { openrig: "0.6.6" }) }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested", openrigVersion: "0.6.6", date: "2026-10-04" } }, communityReports: 0 }),
|
||||
},
|
||||
{
|
||||
name: "a community report is counted separately and never labels",
|
||||
files: [{ group: "team", record: team("r1", { kind: "community-reported" }) }],
|
||||
expect: ok({ platforms: {}, communityReports: 1 }),
|
||||
},
|
||||
...["synthetic", "installed-only", "running-seat", "daemon-adoption"].map((kind) => ({
|
||||
name: `a ${kind} team record never earns a team label`,
|
||||
files: [{ group: "team", record: team("r1", { kind }) }],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
})),
|
||||
{
|
||||
name: "an unreadable synthetic record still makes the listing Status unavailable",
|
||||
files: [{ group: "team", record: team("r1", { kind: "synthetic" }) }, { group: "team", raw: "{ broken" }],
|
||||
expect: { state: "status_unavailable", configurations: {} },
|
||||
},
|
||||
{
|
||||
name: "a harness check is its own line and never upgrades a team label",
|
||||
files: [{ group: "_harness", record: harness("h1", { publicNote: "Pi was not signed in on the test machine." }) }],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
expectHarness: { state: "ok", harnesses: { pi: { "linux-x64": { result: "BLOCKED", harnessVersion: "0.9.1", recordIds: ["h1"], note: "Pi was not signed in on the test machine." } } } },
|
||||
},
|
||||
{
|
||||
name: "a synthetic harness check never replaces a real one",
|
||||
files: [
|
||||
{ group: "_harness", record: harness("h1", { publicNote: "Pi was not signed in on the test machine." }) },
|
||||
{ group: "_harness", record: { ...harness("h2", { at: "2026-10-04T12:00:00Z", steps: [["on-path", "NOT_RUN"]] }), evidence: { kind: "synthetic", receipt: { ref: "receipt.md", sha256: "" } } } },
|
||||
],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
expectHarness: { state: "ok", harnesses: { pi: { "linux-x64": { result: "BLOCKED", recordIds: ["h1"] } } } },
|
||||
},
|
||||
{
|
||||
name: "a synthetic harness record can't withdraw a real harness line",
|
||||
files: [
|
||||
{ group: "_harness", record: harness("h1") },
|
||||
{ group: "_harness", record: { ...harness("h2", { at: "2026-10-04T12:00:00Z", steps: [["on-path", "NOT_RUN"]] }), relations: { supersedes: [], withdraws: ["h1"], resolves: [] }, evidence: { kind: "synthetic", receipt: { ref: "receipt.md", sha256: "" } } } },
|
||||
],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
expectHarness: { state: "ok", harnesses: { pi: { "linux-x64": { result: "BLOCKED", recordIds: ["h1"] } } } },
|
||||
},
|
||||
{
|
||||
name: "an eligible harness record on the same harness and platform can withdraw a line",
|
||||
files: [
|
||||
{ group: "_harness", record: harness("h1") },
|
||||
{ group: "_harness", record: { ...harness("h2", { at: "2026-10-04T12:00:00Z", steps: [["on-path", "NOT_RUN"]] }), relations: { supersedes: [], withdraws: ["h1"], resolves: [] }, evidence: { kind: "installed-only", receipt: { ref: "receipt.md", sha256: "" } } } },
|
||||
],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
expectHarness: { state: "ok", harnesses: { pi: { "linux-x64": { result: "NOT_RUN", recordIds: ["h2"] } } } },
|
||||
},
|
||||
{
|
||||
name: "an installed-only harness check still shows its line",
|
||||
files: [{ group: "_harness", record: { ...harness("h1", { steps: [["on-path", "PASS"], ["version-supported", "PASS"], ["signed-in", "NOT_RUN"]] }), evidence: { kind: "installed-only", receipt: { ref: "receipt.md", sha256: "" } } } }],
|
||||
expect: ok({ platforms: {}, communityReports: 0 }),
|
||||
expectHarness: { state: "ok", harnesses: { pi: { "linux-x64": { result: "NOT_RUN", recordIds: ["h1"] } } } },
|
||||
},
|
||||
{
|
||||
name: "an unreadable harness-check record makes harness checks Status unavailable",
|
||||
files: [{ group: "team", record: team("r1") }, { group: "_harness", raw: "not json" }],
|
||||
expect: ok({ platforms: { "linux-x64": { label: "tested" } }, communityReports: 0 }),
|
||||
expectHarness: { state: "status_unavailable", harnesses: {} },
|
||||
},
|
||||
];
|
||||
|
||||
// Private values planted in every private field of a record; none may appear in the status file.
|
||||
export const PLANTED = ["/Users/someone/receipts", "dev-qa@v-openrig-build", "mm2-host.local", "qitem-20261004-abc", "private help text"];
|
||||
export function plantedRecord() {
|
||||
const record = team("r1", { assistance: 1, recordedBy: PLANTED[1], assistanceText: `${PLANTED[4]} at ${PLANTED[0]} on ${PLANTED[2]} for ${PLANTED[3]}` });
|
||||
record.execution = { resources: [{ path: PLANTED[0] }], settings: { host: PLANTED[2] }, unknowns: [PLANTED[3]] };
|
||||
record.environment.harnesses[0].knownBy = `ran on ${PLANTED[2]}`;
|
||||
return record;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"generated": {
|
||||
"by": "openrig-world tools/status.mjs",
|
||||
"notice": "edited by hand",
|
||||
"rule": "openrig.status-rule/v1",
|
||||
"generatorVersion": 1
|
||||
},
|
||||
"listings": {
|
||||
"openrig-dev": {
|
||||
"state": "ok",
|
||||
"configurations": {
|
||||
"build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex": {
|
||||
"platforms": {
|
||||
"linux-x64": {
|
||||
"label": "tested",
|
||||
"date": "2026-10-04",
|
||||
"openrigVersion": "0.6.6",
|
||||
"recordIds": [
|
||||
"rr-20261004-002"
|
||||
],
|
||||
"packageDigests": [
|
||||
{
|
||||
"algorithm": "sha256",
|
||||
"value": "5f2c0a1e9b8d7c6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a29",
|
||||
"coverage": "openrig.package-digest/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"communityReports": 0
|
||||
},
|
||||
"build.impl=pi,build.lead=pi,check.qa=pi,check.review=pi": {
|
||||
"platforms": {},
|
||||
"communityReports": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"unreadable-example": {
|
||||
"state": "status_unavailable",
|
||||
"configurations": {}
|
||||
}
|
||||
},
|
||||
"harnessChecks": {
|
||||
"state": "ok",
|
||||
"harnesses": {
|
||||
"pi": {
|
||||
"linux-x64": {
|
||||
"result": "BLOCKED",
|
||||
"date": "2026-10-04",
|
||||
"openrigVersion": "0.6.6",
|
||||
"harnessVersion": "unknown",
|
||||
"recordIds": [
|
||||
"rr-20261004-004"
|
||||
],
|
||||
"note": "Pi was not signed in on the test machine."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bodyDigest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"generated": {
|
||||
"by": "openrig-world tools/status.mjs",
|
||||
"notice": "Generated file. Do not edit by hand; regenerate it.",
|
||||
"rule": "openrig.status-rule/v1",
|
||||
"generatorVersion": 1
|
||||
},
|
||||
"listings": {
|
||||
"openrig-dev": {
|
||||
"state": "ok",
|
||||
"configurations": {
|
||||
"build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex": {
|
||||
"platforms": {
|
||||
"linux-x64": {
|
||||
"label": "tested",
|
||||
"date": "2026-10-04",
|
||||
"openrigVersion": "0.6.6",
|
||||
"recordIds": [
|
||||
"rr-20261004-003"
|
||||
],
|
||||
"packageDigests": [
|
||||
{
|
||||
"algorithm": "sha256",
|
||||
"value": "5f2c0a1e9b8d7c6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a29",
|
||||
"coverage": "openrig.package-digest/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"communityReports": 0
|
||||
},
|
||||
"build.impl=pi,build.lead=pi,check.qa=pi,check.review=pi": {
|
||||
"platforms": {},
|
||||
"communityReports": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"unreadable-example": {
|
||||
"state": "status_unavailable",
|
||||
"configurations": {}
|
||||
}
|
||||
},
|
||||
"harnessChecks": {
|
||||
"state": "ok",
|
||||
"harnesses": {
|
||||
"pi": {
|
||||
"linux-x64": {
|
||||
"result": "BLOCKED",
|
||||
"date": "2026-10-04",
|
||||
"openrigVersion": "0.6.6",
|
||||
"harnessVersion": "unknown",
|
||||
"recordIds": [
|
||||
"rr-20261004-004"
|
||||
],
|
||||
"note": "Pi was not signed in on the test machine."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bodyDigest": "548f7ce51c9cf66f34fd0b613dd3754240350cd856ac51f7f98140300766dd57"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// The registry check and the generator read local files and nothing else. This pins what they may import
|
||||
// and refuses the obvious ways to reach the network or run a program.
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const TOOLS = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const ALLOWED = new Set(["node:fs", "node:path", "node:url", "node:crypto", "yaml", "ajv/dist/2020.js", "ajv-formats"]);
|
||||
const FORBIDDEN = /child_process|node:(http|https|net|tls|dgram|dns|worker_threads)\b|\bfetch\s*\(|\beval\s*\(|new\s+Function\b|\bimport\s*\(/;
|
||||
|
||||
const sources = ["", "lib"].flatMap((dir) =>
|
||||
fs.readdirSync(path.join(TOOLS, dir)).filter((name) => name.endsWith(".mjs")).map((name) => path.join(TOOLS, dir, name)));
|
||||
|
||||
test("the tools import only local files, node:fs/path/url/crypto, yaml and ajv", () => {
|
||||
assert.ok(sources.length >= 6, `found ${sources.length} sources`);
|
||||
for (const file of sources) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
for (const [, specifier] of text.matchAll(/^\s*import\s[^;]*?\sfrom\s+"([^"]+)"/gm)) {
|
||||
assert.ok(specifier.startsWith("./") || ALLOWED.has(specifier), `${path.relative(TOOLS, file)} imports ${specifier}`);
|
||||
}
|
||||
assert.doesNotMatch(text, FORBIDDEN, path.relative(TOOLS, file));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { statusBodyDigest } from "../lib/canonical.mjs";
|
||||
import { readYaml } from "../lib/load.mjs";
|
||||
import { RECEIVED, checkRepository, nameTaken, submissionNotices } from "../registry-check.mjs";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO = path.resolve(HERE, "..", "..");
|
||||
const fixture = (name) => fs.readFileSync(path.join(HERE, "fixtures", name), "utf8");
|
||||
const VALID_ENTRY = fixture("registry-valid.yaml");
|
||||
const VIEW_PATH = readYaml(path.join(HERE, "fixtures", "registry-valid.yaml")).configurations[0].behaviour;
|
||||
const VIEW = fixture("behaviour-generated.json");
|
||||
|
||||
/** A temporary registry tree: registry files by name, views by registry path, an optional status file. */
|
||||
function check({ entries = { "openrig-dev.yaml": VALID_ENTRY }, views = { [VIEW_PATH]: VIEW }, status, withNotices = false } = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "registry-check-"));
|
||||
try {
|
||||
for (const [name, text] of Object.entries(entries)) write(root, `registry/${name}`, text);
|
||||
for (const [name, text] of Object.entries(views)) write(root, `registry/${name}`, text);
|
||||
fs.cpSync(path.join(REPO, "status", "journeys"), path.join(root, "status", "journeys"), { recursive: true });
|
||||
if (status !== undefined) write(root, "status/status.json", typeof status === "string" ? status : JSON.stringify(status, null, 2));
|
||||
if (withNotices) return { findings: checkRepository(root), notices: submissionNotices(root) };
|
||||
return checkRepository(root);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function write(root, rel, text) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), text);
|
||||
}
|
||||
|
||||
const has = (findings, text) => assert.ok(findings.some((f) => f.includes(text)), `expected a finding with "${text}", got:\n${findings.join("\n")}`);
|
||||
|
||||
test("this repository as committed passes", () => {
|
||||
assert.deepEqual(checkRepository(REPO), []);
|
||||
});
|
||||
|
||||
const SUBMISSION = "repository: https://github.com/someone/rigs\nfolder: rigs/my-team\nref: main\n";
|
||||
const withEntry = (files) => ({ "openrig-dev.yaml": VALID_ENTRY, ...files });
|
||||
|
||||
test("a submission with repository, folder and ref passes and is acknowledged", () => {
|
||||
for (const name of ["my-team.yaml", "my-team.yml"]) {
|
||||
const { findings, notices } = check({ entries: withEntry({ [`submissions/${name}`]: SUBMISSION }), withNotices: true });
|
||||
assert.deepEqual(findings, [], name);
|
||||
assert.deepEqual(notices, [{ file: `registry/submissions/${name}`, message: RECEIVED }], name);
|
||||
}
|
||||
});
|
||||
|
||||
test("a submission whose name is already listed still passes, with a hint to choose a distinct name", () => {
|
||||
for (const name of ["openrig-dev.yaml", "OpenRig-Dev.yml"]) {
|
||||
const { findings, notices } = check({ entries: withEntry({ [`submissions/${name}`]: SUBMISSION }), withNotices: true });
|
||||
assert.deepEqual(findings, [], name);
|
||||
const stem = name.replace(/\.ya?ml$/, "");
|
||||
assert.deepEqual(notices, [{ file: `registry/submissions/${name}`, message: `${RECEIVED} ${nameTaken(stem)}` }], name);
|
||||
assert.ok(notices[0].message.endsWith(`for example ${stem}-<your-name>.`), name);
|
||||
}
|
||||
const free = check({ entries: withEntry({ "submissions/openrig-dev-alice.yaml": SUBMISSION }), withNotices: true });
|
||||
assert.deepEqual(free.notices, [{ file: "registry/submissions/openrig-dev-alice.yaml", message: RECEIVED }]);
|
||||
});
|
||||
|
||||
test("a malformed submission fails with what a submission needs", () => {
|
||||
for (const [name, text] of [
|
||||
["no-ref.yaml", "repository: https://github.com/someone/rigs\nfolder: rigs/my-team\n"],
|
||||
["not-github.yaml", "repository: https://gitlab.com/someone/rigs\nfolder: rigs/my-team\nref: main\n"],
|
||||
["parent-folder.yaml", "repository: https://github.com/someone/rigs\nfolder: ../elsewhere\nref: main\n"],
|
||||
["extra-field.yaml", `${SUBMISSION}packageDigest: abc\n`],
|
||||
["notes.json", "{}"],
|
||||
]) {
|
||||
const { findings, notices } = check({ entries: withEntry({ [`submissions/${name}`]: text }), withNotices: true });
|
||||
has(findings, "a submission has exactly three fields: repository");
|
||||
assert.deepEqual(notices, [], name);
|
||||
}
|
||||
});
|
||||
|
||||
test("a file name that could break CI output is quoted, never echoed", () => {
|
||||
const { findings, notices } = check({ entries: withEntry({ "submissions/x\n::warning::y.yaml": SUBMISSION }), withNotices: true });
|
||||
has(findings, 'registry/submissions/"x\\n::warning::y.yaml"');
|
||||
assert.ok(findings.every((f) => !f.includes("\n")), findings.join(" | "));
|
||||
assert.deepEqual(notices, []);
|
||||
});
|
||||
|
||||
test("a submission placed beside the entries is pointed to submissions/; a broken full entry is not", () => {
|
||||
has(check({ entries: withEntry({ "my-team.yaml": SUBMISSION }) }), "add registry/submissions/my-team.yaml with only repository, folder and ref");
|
||||
const broken = check({ entries: { "openrig-dev.yaml": VALID_ENTRY.replace(/^review:[\s\S]*?(?=^status:)/m, "") } });
|
||||
has(broken, "not a registry entry v1");
|
||||
assert.ok(broken.every((f) => !f.includes("registry/submissions/")), broken.join("\n"));
|
||||
});
|
||||
|
||||
test("a valid entry with its behaviour view passes", () => {
|
||||
assert.deepEqual(check(), []);
|
||||
});
|
||||
|
||||
test("malformed YAML is rejected", () => {
|
||||
has(check({ entries: { "openrig-dev.yaml": "slug: [openrig-dev\nname: x\n" } }), "not readable YAML");
|
||||
});
|
||||
|
||||
test("a duplicated key is rejected", () => {
|
||||
has(check({ entries: { "openrig-dev.yaml": `${VALID_ENTRY}status: withdrawn\n` } }), "not readable YAML");
|
||||
});
|
||||
|
||||
test("a short commit is rejected", () => {
|
||||
has(check({ entries: { "openrig-dev.yaml": fixture("registry-short-commit.yaml") } }), "not a registry entry v1");
|
||||
});
|
||||
|
||||
test("a 40-character commit that isn't hex is rejected", () => {
|
||||
const entry = VALID_ENTRY.replace(/resolvedCommit: [0-9a-f]{40}/, `resolvedCommit: ${"g".repeat(40)}`);
|
||||
assert.notEqual(entry, VALID_ENTRY);
|
||||
has(check({ entries: { "openrig-dev.yaml": entry } }), "resolvedCommit");
|
||||
});
|
||||
|
||||
test("the slug must match the file name", () => {
|
||||
has(check({ entries: { "another-name.yaml": VALID_ENTRY } }), "must match the file name");
|
||||
});
|
||||
|
||||
test("a configuration ID out of order is rejected", () => {
|
||||
const entry = VALID_ENTRY.replace(
|
||||
"id: build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex",
|
||||
"id: build.lead=claude-code,build.impl=claude-code,check.qa=codex,check.review=codex");
|
||||
assert.notEqual(entry, VALID_ENTRY);
|
||||
has(check({ entries: { "openrig-dev.yaml": entry } }), "not sorted");
|
||||
});
|
||||
|
||||
test("a missing behaviour view is rejected", () => {
|
||||
has(check({ views: {} }), "missing or not readable JSON");
|
||||
});
|
||||
|
||||
test("a behaviour view for another package is rejected", () => {
|
||||
const view = JSON.parse(VIEW);
|
||||
view.identity.packageDigest.value = "0".repeat(64);
|
||||
has(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), "different package digest");
|
||||
});
|
||||
|
||||
test("setup commands in a behaviour view must be a list of text lines", () => {
|
||||
const view = JSON.parse(VIEW);
|
||||
view.needs.push({ kind: "precondition", name: "Prepare the source clone", commands: ["cd openrig", "npm ci"], status: "not_checked", sourceRefs: [{ path: "bundle.yaml", field: "preconditions[0]" }] });
|
||||
assert.deepEqual(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), []);
|
||||
view.needs.at(-1).commands = "cd openrig && npm ci";
|
||||
has(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), "not a behaviour view v1");
|
||||
});
|
||||
|
||||
test("permission prompts in a behaviour view must be off, on or default", () => {
|
||||
const view = JSON.parse(VIEW);
|
||||
view.posture[0].permissionPrompts = "default";
|
||||
assert.deepEqual(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), []);
|
||||
view.posture[0].permissionPrompts = "sometimes";
|
||||
has(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), "not a behaviour view v1");
|
||||
});
|
||||
|
||||
test("a seat on a need or an unknown must be pod.member", () => {
|
||||
for (const list of ["needs", "unknownBeforeLaunch"]) {
|
||||
const view = JSON.parse(VIEW);
|
||||
view[list][0].seat = "build.lead";
|
||||
assert.deepEqual(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), [], list);
|
||||
view[list][0].seat = "lead";
|
||||
has(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), "not a behaviour view v1");
|
||||
}
|
||||
});
|
||||
|
||||
test("the non-interruptive posture facts accept only their declared values", () => {
|
||||
const view = JSON.parse(VIEW);
|
||||
view.posture[0].nonInterruptive = "available";
|
||||
view.posture[0].firstRunWarnings = { claudeBypass: "harness_asks_once" };
|
||||
assert.deepEqual(check({ views: { [VIEW_PATH]: JSON.stringify(view) } }), []);
|
||||
for (const [field, value] of [["nonInterruptive", "selected"], ["firstRunWarnings", { claudeBypass: "accepted" }]]) {
|
||||
const changed = JSON.parse(JSON.stringify(view));
|
||||
changed.posture[0][field] = value;
|
||||
has(check({ views: { [VIEW_PATH]: JSON.stringify(changed) } }), "not a behaviour view v1");
|
||||
}
|
||||
});
|
||||
|
||||
test("a generated status file passes", () => {
|
||||
assert.deepEqual(check({ status: fixture("status-valid.json") }), []);
|
||||
});
|
||||
|
||||
test("a hand-edited status file is rejected", () => {
|
||||
const status = JSON.parse(fixture("status-valid.json"));
|
||||
const [listing] = Object.values(status.listings);
|
||||
const [configuration] = Object.values(listing.configurations).filter((c) => Object.keys(c.platforms).length);
|
||||
Object.values(configuration.platforms)[0].openrigVersion = "9.9.9";
|
||||
has(check({ status }), "edited by hand");
|
||||
});
|
||||
|
||||
test("a status file with its notice changed is rejected", () => {
|
||||
assert.ok(check({ status: fixture("status-hand-edited-notice.json") }).length > 0);
|
||||
});
|
||||
|
||||
test("private text in a status file is rejected even with a recomputed digest", () => {
|
||||
const status = JSON.parse(fixture("status-valid.json"));
|
||||
const pi = Object.values(status.harnessChecks.harnesses)[0];
|
||||
Object.values(pi)[0].note = "Log at /Users/someone/run.log";
|
||||
status.bodyDigest = statusBodyDigest(status);
|
||||
has(check({ status }), "private text (absolute path)");
|
||||
});
|
||||
|
||||
test("a behaviour view that resolves outside registry/ through a symlink is rejected", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "registry-check-link-"));
|
||||
try {
|
||||
write(root, "registry/openrig-dev.yaml", VALID_ENTRY);
|
||||
write(root, "outside/view.json", VIEW);
|
||||
fs.mkdirSync(path.dirname(path.join(root, "registry", VIEW_PATH)), { recursive: true });
|
||||
fs.symlinkSync(path.join(root, "outside/view.json"), path.join(root, "registry", VIEW_PATH));
|
||||
fs.cpSync(path.join(REPO, "status", "journeys"), path.join(root, "status", "journeys"), { recursive: true });
|
||||
has(checkRepository(root), "resolves outside registry/ through a symlink");
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a status file with text that isn't well-formed Unicode is rejected", () => {
|
||||
const status = JSON.parse(fixture("status-valid.json"));
|
||||
const pi = Object.values(status.harnessChecks.harnesses)[0];
|
||||
Object.values(pi)[0].note = "Unpaired " + String.fromCharCode(0xd800);
|
||||
status.bodyDigest = statusBodyDigest(status);
|
||||
has(check({ status }), "well-formed Unicode");
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { statusBodyDigest } from "../lib/canonical.mjs";
|
||||
import { compareVersions } from "../lib/status-rule.mjs";
|
||||
import { generate } from "../status.mjs";
|
||||
import { CASES, MIX, PLANTED, entry, plantedRecord, team } from "./fixtures/status-cases.mjs";
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
||||
|
||||
/** Writes one case as an registry tree plus a records tree, each record in its own run folder. */
|
||||
function materialize({ entry: listing = entry(), files }) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "status-case-"));
|
||||
const root = path.join(dir, "world");
|
||||
fs.mkdirSync(path.join(root, "registry"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "registry", `${listing.slug}.yaml`), JSON.stringify(listing, null, 2));
|
||||
fs.cpSync(path.join(REPO, "status", "journeys"), path.join(root, "status", "journeys"), { recursive: true });
|
||||
const records = path.join(dir, "records");
|
||||
files.forEach((file, i) => {
|
||||
const run = path.join(records, `run-${i}`);
|
||||
const group = path.join(run, "run-records", file.group);
|
||||
fs.mkdirSync(group, { recursive: true });
|
||||
if (file.raw !== undefined) {
|
||||
fs.writeFileSync(path.join(group, `raw-${i}.json`), file.raw);
|
||||
return;
|
||||
}
|
||||
const receipt = `receipt for ${file.record.id}\n`;
|
||||
const record = structuredClone(file.record);
|
||||
record.evidence.receipt.sha256 = sha256(receipt);
|
||||
if (file.receipt !== "missing") fs.writeFileSync(path.join(run, "receipt.md"), file.receipt === "changed" ? `${receipt}edited later\n` : receipt);
|
||||
fs.writeFileSync(path.join(group, `${record.id}.json`), JSON.stringify(record, null, 2));
|
||||
});
|
||||
return { dir, root, records };
|
||||
}
|
||||
|
||||
function run(caseInput) {
|
||||
const { dir, root, records } = materialize(caseInput);
|
||||
try {
|
||||
return generate({ root, recordRoots: [records] });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Key sets must match exactly, except inside a label or harness line, where the expectation is a subset. */
|
||||
function assertShape(actual, expected, at) {
|
||||
if (expected === null || typeof expected !== "object" || Array.isArray(expected)) {
|
||||
assert.deepEqual(actual, expected, at);
|
||||
return;
|
||||
}
|
||||
assert.ok(actual && typeof actual === "object", `${at} is missing`);
|
||||
if (!("label" in expected) && !("result" in expected)) {
|
||||
assert.deepEqual(Object.keys(actual).sort(), Object.keys(expected).sort(), `${at} keys`);
|
||||
}
|
||||
for (const [key, value] of Object.entries(expected)) assertShape(actual[key], value, `${at}.${key}`);
|
||||
}
|
||||
|
||||
for (const c of CASES) {
|
||||
test(c.name, () => {
|
||||
const result = run(c);
|
||||
assert.deepEqual(result.problems, []);
|
||||
assertShape(result.status.listings.team, c.expect, "listings.team");
|
||||
if (c.expectHarness) assertShape(result.status.harnessChecks, c.expectHarness, "harnessChecks");
|
||||
assert.equal(result.status.bodyDigest, statusBodyDigest(result.status));
|
||||
});
|
||||
}
|
||||
|
||||
test("regenerating the same records gives identical bytes", () => {
|
||||
const input = { files: [{ group: "team", record: team("r1", { assistance: 1 }) }, { group: "team", record: team("r2", { platform: "darwin" }) }] };
|
||||
assert.equal(run(input).text, run(input).text);
|
||||
});
|
||||
|
||||
test("private values planted in every private field never reach the status file", () => {
|
||||
const result = run({ files: [{ group: "team", record: plantedRecord() }] });
|
||||
assert.deepEqual(result.problems, []);
|
||||
assert.equal(result.status.listings.team.configurations[Object.keys(result.status.listings.team.configurations)[0]].platforms["linux-x64"].label, "tested_with_help");
|
||||
for (const value of PLANTED) assert.ok(!result.text.includes(value), `status carries ${value}`);
|
||||
});
|
||||
|
||||
test("a Partly tested label carries its record's public note, and none without one", () => {
|
||||
const steps = [["install", "PASS"], ["launch", "PASS"], ["orient", "PASS"], ["pull-request", "NOT_RUN"]];
|
||||
const label = (record) => run({ files: [{ group: "team", record }] }).status.listings.team.configurations[MIX].platforms["linux-x64"];
|
||||
const noted = label(team("r1", { steps, publicNote: "Scope: install to orientation; no task was run." }));
|
||||
assert.equal(noted.label, "partly_tested");
|
||||
assert.equal(noted.note, "Scope: install to orientation; no task was run.");
|
||||
const plain = label(team("r1", { steps }));
|
||||
assert.equal(plain.label, "partly_tested");
|
||||
assert.ok(!("note" in plain));
|
||||
});
|
||||
|
||||
test("a public note carrying private text stops generation", () => {
|
||||
const steps = [["install", "FAIL"]];
|
||||
const result = run({ files: [{ group: "team", record: team("r1", { steps, publicNote: `See ${PLANTED[0]} for the log.` }) }] });
|
||||
assert.ok(result.problems.some((p) => p.includes("private text (absolute path)")), result.problems.join("\n"));
|
||||
});
|
||||
|
||||
test("a public note naming a private host stops generation", () => {
|
||||
const result = run({ files: [{ group: "team", record: team("r1", { steps: [["install", "FAIL"]], publicNote: "Install stopped on mm2-host.local." }) }] });
|
||||
assert.ok(result.problems.some((p) => p.includes("private text (private host name)")), result.problems.join("\n"));
|
||||
});
|
||||
|
||||
test("a public note with any absolute path stops generation; ordinary prose and GitHub links pass", () => {
|
||||
const steps = [["install", "FAIL"]];
|
||||
for (const note of [
|
||||
"See /workspace/private/receipt.txt for the failure.",
|
||||
'See "/Workspace Files/receipt.txt" for the failure.',
|
||||
"See /données/receipt.txt for the failure.",
|
||||
"See C:/workspace/receipt.txt for the failure.",
|
||||
"See C:\\workspace\\receipt.txt for the failure.",
|
||||
"Log at ~/run.log.",
|
||||
"See **/Users/admin/receipt.txt** for the failure.",
|
||||
"The log went to 2>/var/log/x.log.",
|
||||
"Piped through a|/etc/x.",
|
||||
"See \u201c/Workspace Files/receipt.txt\u201d for the failure.",
|
||||
"See \u2018/tmp/receipt.txt\u2019 for the failure.",
|
||||
"See ///workspace/private/receipt.txt for the failure.",
|
||||
"Mounted at //srv/x.",
|
||||
]) {
|
||||
const blocked = run({ files: [{ group: "team", record: team("r1", { steps, publicNote: note }) }] });
|
||||
assert.ok(blocked.problems.some((p) => p.includes("private text (absolute path)")), `${note}: ${blocked.problems.join("; ")}`);
|
||||
}
|
||||
for (const note of ["The pull request did not merge.", "See https://github.com/example/rigs/issues/1 (TCP/IP and/or 1/2).", "Fixed w/o a rerun on 2026/10/04.", "Notes on r\u00e9sum\u00e9/donn\u00e9es pages."]) {
|
||||
const result = run({ files: [{ group: "team", record: team("r1", { steps, publicNote: note }) }] });
|
||||
assert.deepEqual(result.problems, [], note);
|
||||
}
|
||||
});
|
||||
|
||||
test("text that isn't well-formed Unicode stops generation", () => {
|
||||
const result = run({ files: [{ group: "team", record: team("r1", { steps: [["install", "FAIL"]], publicNote: "Unpaired " + String.fromCharCode(0xd800) }) }] });
|
||||
assert.ok(result.problems.some((p) => p.includes("well-formed Unicode")), result.problems.join("\n"));
|
||||
});
|
||||
|
||||
test("release versions order prereleases by SemVer", () => {
|
||||
assert.ok(compareVersions("0.6.6-rc.10", "0.6.6-rc.9") > 0);
|
||||
assert.ok(compareVersions("0.6.6", "0.6.6-rc.1") > 0);
|
||||
assert.ok(compareVersions("0.6.6-rc.1", "0.6.6-rc") > 0);
|
||||
assert.ok(compareVersions("0.6.6-1", "0.6.6-alpha") < 0);
|
||||
assert.ok(Number.isNaN(compareVersions("0.6", "0.6.6")));
|
||||
assert.equal(compareVersions("0.6.6+build2", "0.6.6+build1"), 0);
|
||||
assert.ok(compareVersions("0.6.7+b", "0.6.6") > 0);
|
||||
});
|
||||
|
||||
test("records for a slug with no listed entry are reported, not labelled", () => {
|
||||
const result = run({ files: [{ group: "other-team", record: team("r1") }] });
|
||||
assert.deepEqual(result.problems, []);
|
||||
assert.ok(result.warnings.some((w) => w.includes("other-team")));
|
||||
assert.deepEqual(Object.keys(result.status.listings), ["team"]);
|
||||
});
|
||||
Reference in new issue
Block a user