Compare commits

..
Author SHA1 Message Date
jason.woltje 21c123ae84 row 287 (#1792): BUILD.md records sage-2958: gitea entries need scopes (role.mjs:100-111), equal to SEAT_SCOPES; seven chief-of-staff.json readers; doc only 2026-10-22 14:10:41 -05:00
jason.woltje 13e4a23ee6 row 287 (#1792): BUILD.md records sage-2950: 285 runs the bus suite whole and has the no-scopes check, dewey-0865 withdrawn, mos-1333 drops the 285 publish fence so sage-2916 stands; doc only 2026-10-22 14:05:45 -05:00
jason.woltje 81b03c9404 row 287 (#1792): BUILD.md answers dewey-0865: the definitions' gitea credential is row 285's (C2-08b brief :1823, mos-1309 item 5), 287 builds after it; scopes and the five bus readers of chief-of-staff.json noted for 285; doc only 2026-10-22 13:59:40 -05:00
jason.woltje f70f83635f row 287 (#1792): BUILD.md folds Dewey's early look dewey-0862: Q2 repositories as remote URLs and the notice, the read line, block lines B-a to B-c, Q1 test L10, Q3 fork-of-library in L4, Q4 count line, exact permission keys, chief by definition chief-of-staff and a fired chief in B-2; doc only 2026-10-22 13:53:47 -05:00
jason.woltje 5bcfef0ada row 287 (#1792): BUILD.md records sage-2916: 287 owns business.mjs :283-291 and its business.test.mjs cases (files-owned extension), the refusal rewritten to creative or chief with a gitea credential, B-1 to B-4 and M5, the old-message grep before in-review, PRD 1.15 REQ-LIB-6 check; Q1-Q4 accepted as assumptions; doc only 2026-10-22 13:43:06 -05:00
jason.woltje eea8c5b784 row 287 (#1792): BUILD.md design draft for C2-21 off 94c33a43c: B1 business.mjs:286 refuses a library project in publish, Q1-Q4 (hash, event, mixed raises, Docker token search), the change, tests L1-L9 and mutants; doc only, not run 2026-10-22 13:41:55 -05:00
2 changed files with 192 additions and 191 deletions

No files matched your search

-191
View File
@@ -1,191 +0,0 @@
# Row 283 (#1788) C2-01d design, draft 2 (Honker)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at
agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300),
1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The
build waits for 282 to land (sage-2906).
## 1. Mapping (packages/library/src/mapping.mjs)
`supporting`:
- Every universe kind takes an optional `supporting` list: plain `['name','books','playground','supporting']`,
hybrid adds it to its seven keys, and a draft gets `['kind','name','playground','supporting']`.
- Each value is checked as a playground prefix is: a string ending in `/` whose body passes `folder()`.
- The output carries `supporting` on every universe, `[]` when absent, the way a draft carries
`books: []`. classify then needs no `?? []`. The business writer writes the raw document
(organization.mjs:195), not this output, so no business file changes shape.
- The prefix rules, across every entry (ruling after r11):
- A supporting prefix holding another supporting prefix, or one listed twice, refuses
`library-mapping-invalid`.
- A prefix equal to one of its own entry's book folders, or holding one, refuses `library-mapping-invalid`.
- A prefix inside or holding another entry's book folder refuses `library-supporting-other-universe`.
- Level comes from where the prefix sits, as for the playground: inside one of its own entry's books
is book level, and outside every book is universe level. A draft's prefixes are all universe level.
- Between the two lists (my choice, Q2):
- A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
- A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting
and the rest stays playground.
- A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. classify
checks supporting first, so that playground prefix could never match.
- A supporting prefix equal to, holding or inside a playground prefix of another entry refuses
`library-mapping-invalid`. Otherwise one path would have two universes.
- The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and
label. The playground's messages and codes stay byte-equal, so no existing test text moves.
`editor`:
- The entry takes an optional `editor`. When present, it is an object with exactly the keys `name` and
`email` (via `only()`). Each value is a string that is non-empty after trim and has no control
character (`[\u0000-\u001f\u007f-\u009f]`), `<` or `>` (webby-0169 R1: git drops them from an ident
and commits under a different name, so C2-02 would never see a failure). Anything else refuses
`library-mapping-invalid`.
- The output carries `editor: { name, email }` only when present, as `user` and `vaultPrefix` do. A save
with no `editor` refusing is C2-02's job.
## 2. Scope (scope.mjs)
- `classify` gets a supporting loop before the playground loop (design 1.2). It returns `place(...)`
with `{ scope: 'supporting', universe, book }`. The book is the book folder holding the prefix, or
null at universe level and in a draft. The type and folder come from the first folder under the
prefix, as for the playground.
- The `.md` check stays first, so a `.canvas` or `.base` under a supporting prefix stays unmapped.
- The comment above `classify` names the three prefix scopes and the order.
## 3. supportingPath (new supporting.mjs)
`supportingPath(mapping, universe, path, tree)` returns `path` or throws
`LibraryError('library-write-story-path', ...)`. It is pure and reads no git.
- `tree` is the array `listTree` returns (reader.mjs:26, `{ mode, type, oid, size, path }`), so the
C2-02 broker passes its listing as it is (Q3). A non-array `tree` throws a TypeError, because that is
a caller bug, not a path class.
- The checks run in this order, each with its own message naming the class:
1. The path must be a string of at most 1,024 UTF-8 bytes (`Buffer.byteLength`).
2. It must have no control character, using the same control class as `editor` (not `<` or `>`).
3. It must have no backslash.
4. It must not be absolute (a leading `/`).
5. It must have no empty, `.` or `..` segment. That also catches `//` and a trailing `/`.
6. `classify(mapping, path)` must give scope `supporting` and that same `universe`. This one check
covers an unknown universe, an unmapped path, a `.canvas`, `.base` or attachment, a canon or
story path, a playground path that isn't also supporting, and another universe's prefix. The scope
rule then has one source, and M1 below hits both.
7. Every proper prefix of the path (`a`, `a/b`, ...) and the path itself must not be in `tree` with
mode `120000` (symlink) or `160000` (gitlink).
8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory
(derived from the entries' paths) after `normalize('NFC').toLowerCase()` on both sides, unless
it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of
a name as one file (webby-0169 N1).
Directories are checked too, because the folders under a supporting prefix are hers to name. A
path in a tree that holds both `a.md` and `A.md` refuses, which matches R7.
- The refusal messages quote the path with JSON, except for the control-character class, whose message
leaves the path out.
- The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in
the brief's supportingPath list. It stays with C2-02's delete (Q4).
The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs
(webby-0169 Q4).
- index.mjs exports `supportingPath`.
## 4. README (packages/library/README.md)
- Mapping: `supporting` and its rules, the cross-list rules, and `editor`.
- Refusals: a `library-supporting-other-universe` row, and the new cases in the
`library-mapping-invalid` row.
- Scope: supporting first, then playground, then canon.
- A new section, "Agent write paths", for supportingPath and its classes.
## 5. Callers of classify outside files owned (Q1 for Sage)
- survey.mjs:65 builds `{ canon: 0, playground: 0, unmapped: 0 }` and increments
`scopes[classify(...).scope]`. A supporting file adds `supporting: NaN`, and formatSurvey (:92) never
prints it. I propose a departure: add `supporting: 0` and print it, so the line becomes
`scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6`. That re-pins survey.test:73 and
adds a count test. Without it, the survey reports garbage once she marks a folder. The departure
covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
- No change in the rest, each for a reason:
- reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
- R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file
raises nothing.
- render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
- mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
- No bus or webui source filters on scope (git grep at 94c33a43c).
- M9 already protects the whole `projects` key from agent edits (library-projects-protected.test). No
test is added there.
## 6. Tests
- mapping.test:
- T1: `supporting` at book level, at universe level, in a hybrid and in a draft. The output carries
it, with `[]` when absent. The draft `deepEqual` at :48 gains `supporting: []`.
- T2: one test per prefix rule.
- These refuse `library-mapping-invalid`: no trailing `/`, a bad folder, one holding another, one
listed twice, one equal to its own book, one holding its own book, one holding a playground
prefix, and one overlapping another entry's playground.
- These refuse `library-supporting-other-universe`: one inside another universe's book, and one
holding another universe's book.
- An equal same-entry playground passes.
- T3: `editor` passes, and absent passes. It refuses `library-mapping-invalid` for a non-object, an
array, an extra key, a missing key, an empty or blank value, a non-string, and a control character,
`<` or `>` in either value.
- scope.test, T4:
- Supporting at book level, universe level and in a draft each give their universe and book.
- A folder in both lists is `supporting`, and a playground-only folder is `playground`.
- A supporting `.canvas` or `.base` is unmapped.
- supporting.test (new):
- T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
- T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant.
The classes are:
- unmapped, `.canvas`, `.base`, an attachment, canon, playground-only, another universe's
supporting prefix, and an unknown universe;
- absolute, `..`, `.`, `//`, a trailing `/`, a backslash, a C0 control and DEL;
- a symlink folder, a gitlink folder, and a symlink at the path itself;
- a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
- 1,025 bytes made with a multi-byte character, so bytes and characters differ.
- Each asserts the code `library-write-story-path`.
- business.test: T7 adds one assertion that `validateBusinessDocument` refuses
`library-supporting-other-universe`.
- survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.
## 7. Mutants (exact strings go in BUILD.md)
| Mutant | Change | Killed by |
|---|---|---|
| M1 | classify checks playground before supporting (the brief's mutant) | T4 both-lists, T5 both-lists |
| M2 | the other-universe fail for supporting is dropped | T2 |
| M3 | the symlink and gitlink check is dropped | T6 |
| M4 | the case check is dropped | T6 |
| M4b | check 8 drops `normalize('NFC')` | T6 NFD variant |
| M5 | the byte length uses `path.length` | T6 1,025 bytes |
| M6 | the `editor` control-character check is dropped | T3 |
| M6b | the `editor` `<` and `>` check is dropped | T3 |
| M7 | supportingPath skips the universe match | T6 other universe |
| M8 | the supporting-holds-playground check is dropped | T2 |
## 8. Gate
- Webby approves the manifest.
- The library and business suites and every scripts/test-*.sh are green on Sage's gate.
- Timeouts (mos-1275): each package `timeout -k 10 1800`, each script `timeout -k 10 300`, each single
file and mutant `timeout -k 10 600`. These come from my row 256 gate at 10:34Z.
- Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed
message text, then drift-check BASE..refactor.
## 9. Questions
- Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
- Q2 (Webby): the cross-list rules in section 1?
- Q3 (Webby): `tree` as the `listTree` array?
- Q4 (Webby): the delete's 100644 check stays with C2-02?
- Q5 (Webby): `editor` refuses only blank values and control characters, as the brief says. `<` and `>`
would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.
## 10. Draft 2 (webby-0169)
- R1 folded: `editor` refuses `<` and `>` too, with T3 cases and mutant M6b.
- Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground
prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost
is that she can't nest a playground under a supporting folder, though she can nest the other way.
- Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
- N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
- N2: the survey.test:73 pin gains only `supporting`, and the count test is the only addition
(mos-1330, its own commit).
- N3: before the build, re-grep tests outside packages/library for a `deepEqual` of the mapping output
on the build base.
+192
View File
@@ -0,0 +1,192 @@
# Row 287 design notes: C2-21, the grant executor for a library project (#1792)
Builder Megavolt, reviewer Dewey, gate owner Sage (sage-2910, Q226, mos-1310). Branch megavolt/row287, a
doc-only draft off 94c33a43c, written during Gizmoduck's slot with local git only. The build starts on the
candidates of row 285 (C2-08b, Scrooge) and row 226 (C2-13b, Gosalyn) and rebases onto their landings
(Q177). Nothing here has been run.
The brief is "C2-21 the grant executor for a library project" in docs/plans/2026-10-20_case2-rows.md at
94c33a43c. Its gate: Dewey approves the manifest; the bus suite, the Gitea Docker test and the install test
are green on Sage's gate, along with every `scripts/test-*.sh`; a search of the test run's output for the
fixture admin and seat tokens finds nothing.
## Sources (line cites at 94c33a43c)
- `packages/bus/src/gitea-grant.mjs`: the admin api (`adminApi`, no collaborator call yet), the seat api
(`forkGet`, `fork`, `repos`), `identitySha256`, `REPO_PAGE`/`REPO_PAGES`.
- `packages/bus/src/broker.mjs`: `#giteaRole` :2132, `#giteaTargets` :2149, `#giteaGrantBinding` and its
block :2167-2196, `#giteaRecheck` :2200, `#giteaSteps` step 4 :2425-2441 and step 5 :2442-2473, the
`grant.applied` body and notice :2480-2488, the host refusals `host-creative-role` :1106 and :1254.
- `packages/business/src/business.mjs` :266-288: a `kind: "library"` project goes through `checkMapping`, and
:286 refuses any role whose `publish` names a library project.
- `packages/library/src/mapping.mjs`: a library entry carries `remote`, `branch` and an optional `user`.
- `packages/bus/tests/gitea-grant.test.mjs` :297-334 pins the `grant.applied` body (`forks: [...]`) and the
block; `fake-gitea-api.mjs` keeps collaborators as a list of logins with no permission.
- `packages/bus/tests/gitea-grant-docker.test.mjs`: the scratch gitea/gitea:1.27.1 container; it uses
`os.tmpdir()`.
## 1. Rulings, blockers and questions
- B1, the publish rule. `business.mjs:286` refuses any `publish` entry that names a library project
("library repositories cannot be published by agents"). The brief's chief needs `publish: ["books"]`, and a
creative seat needs the library project in `publish` for `#giteaRole` and `#giteaTargets` to pass. No test
under packages/*/tests or scripts pins the :286 message (positive control: "publish names" hits
agent-identity.test.mjs).
- Ruling sage-2916 (18:42Z 10-22), files-owned extension: row 287 owns `packages/business/src/business.mjs`
:283-291 and the `packages/business/tests/business.test.mjs` cases for it. Row 285 leaves business.mjs
alone. Conditions:
- The refusal is rewritten, not deleted. A library project in `publish` loads only for a creative
instance (`CREATIVE_DEFINITIONS`) or the chief of staff, each with a gitea credential. Any other
instance still refuses, with a new message that names the rule. Tests cover both sides. The grant path
serves an allowed one with collaborator reach, never a fork. (To confirm on the base: how business.mjs
names the chief, by definition `chief-of-staff` or by `launch.by`.)
- Before in-review, grep 'packages/*/tests/*' and `scripts/test-*.sh` again for the old message text,
with a positive control.
- PRD 1.15 REQ-LIB-6 is the authority for this. If Mos changes 1.15 (sage-2914, `library.write`
routing), check this text against what lands.
- Q1 to Q4 below: Sage accepted them as stated assumptions (sage-2916). Dewey checks them in the round.
- Q1, the hash. `identitySha256` covers identity, publish and remotes, not a project's kind or the role's
definition name, and the level comes from the definition name. Proposal: add a `library` key
(`{project: level}`) to the hash input only when a publish project is a library, so every non-library
grant's hash stays byte-identical and no open grant turns `decision-mismatch` on upgrade.
- Q2, the event. `grant.applied` and its notice carry `forks`, and the test at :322 pins that body.
Proposal: `forks` stays (empty when every target is a library), and a `repositories:
[{project, repository, level}]` key appears only when a library target exists. The notice names them the
same way.
- Q3, mixed raises. A creative raise must name only library projects (mos-1320 item 2). The chief may
name a library project next to forked ones in one raise; step 4 forks the others and adds the collaborator
on the library one. Proposal: allow that, and test it. Under sage-2916, any other non-creative instance
can't hold a library project in `publish` at all, so the business file refuses it before any raise.
- Q4, the token search for the Docker test. The fake API's tokens are fixtures and can be grepped. The
Docker test's tokens are minted by the container at run time, so the gate grep can't know them in advance.
Proposal: the Docker test captures its own diagnostics and asserts that no token it minted appears in
them, and the gate greps the run's output for every fake-API fixture token.
### Early look, Dewey (dewey-0862, 18:53Z 10-22, on 5bcfef0ad, read only), folded
These replace the matching proposals in sections 2 and 3 where they differ.
- Q2 accepted. `forks` stays and is `[]` when every target is a library. `repositories:
[{project, repository, level}]` appears only when a library target exists. `repository` is the remote
URL, the same form as `forks`; the `full_name` that step 5 matched is asserted in a test and doesn't go
in the body. The only src reader of `forks` is the notice (:2483). The notice drops its forks clause when
the list is empty and adds "repositories: <url> (<level>)". A non-library notice keeps today's text.
- The read line: "The token can read every path of this repository. It can't push to it, because the user
is a read collaborator." The seat scope is still `write:repository` (:2189).
- B-a: `\nForks:` (:2187) prints only when a fork target exists.
- B-b: "Step 5 checks that this is only its forks." (:2193) is unchanged with no library target. With one, it
reads "Step 5 checks that this is only its forks and the repositories above at their level."
- B-c: `others` (:2181, :2194) splits. An untargeted library project in publish gets its own line, "It may
also reach these repositories as a collaborator: <url> (<level>)", because a revoke leaves the
collaborator entry and step 5 accepts it publish-wide. Forked others keep today's line. L8 covers B-a to
B-c.
- Q1 holds. Test: a project flipped to `kind: "library"` between the raise and the recheck gives
`decision-mismatch` (L10).
- Q3 holds. Library projects stay out of step 5's fork upstream set (:2446), so a fork of her repository
refuses `gitea-reach-wider`; L4 covers it.
- Q4 holds if the assertion covers what the test prints, TAP stdout and stderr, not only an in-test
buffer. The Docker test prints one line, "minted N, hits 0", for the gate to grep.
- Step 5 compares exactly the keys `admin`, `push` and `pull`. BUILD.md records F-B's actual `permissions`
object, so an extra Gitea key can't pass or fail by accident.
- The chief is named by definition `chief-of-staff`, not `launch.by`, matching broker `STAFF_TO` (:240) and
row 286 (dewey-0860). A fired chief keeps its roles entry (instance-settings.mjs:355), so B-2 also loads a
fired chief's entry.
### The definitions' gitea credential (dewey-0865)
At 94c33a43c, chief-of-staff.json and all six creative definitions have `credentials: []`, so `#giteaRole`
(:2137-2143) would refuse them all `gitea-not-declared`. Row 287 doesn't add the credential. Row 285 (C2-08b)
owns those role files, and its brief (case2-rows.md :1823-1824) gives each creative role and the chief
`[{"service": "gitea"}]`, which Mos authorized in mos-1309 item 5. That is why 287 builds after 285 lands. Two
notes for 285, not for this row:
- The grant path reads only `service === 'gitea'`. The seat token's scopes come from `SEAT_SCOPES` in code,
not from the definition. coder.json adds `scopes`, and 285's validator decides whether the bare form passes.
- Five bus tests read roles/chief-of-staff.json directly (chief-of-staff, fire-revokes-grants,
instances-verb, names, routes-verb), and 285's gate names the business suite, not the bus suite.
- Settled (sage-2950, 19:06Z 10-22): 285's gate runs the bus suite whole (sage-2946), and Scrooge has the
role.mjs no-scopes check. Dewey withdrew dewey-0865 (dewey-0867). Mos withdrew the mos-1332 item 3 publish
fence (mos-1333), so business.mjs stays out of 285's files and sage-2916 stands as written.
- Correction (sage-2958, Scrooge): role.mjs refuses `{"service": "gitea"}` with no scopes
(checkGiteaScopes :100-111, util.mjs:42); only a host entry may omit them (:134). So 285's entries name
their scopes, equal to SEAT_SCOPES (sage-2915). The chief-of-staff.json readers are seven, not five: add
host-run and reload. 287's tests that build definitions give gitea entries `scopes: ["read:user",
"write:repository"]`.
## 2. The change (proposed)
- Order in the executor: step 2 (user) and step 3 (mint) as today. Step 4, for each target: a library
project gets `PUT /api/v1/repos/{owner}/{repo}/collaborators/{user}` with `{permission}` through the admin
api, op `collaborator-add`, the `token` header, logged like every call. Expect 204, or anything else fails
with `gitea-failed`. A non-library project forks as today.
- The level: `write` when `CREATIVE_DEFINITIONS.includes(role.definition)`, `read` otherwise. The raise has
no field for it; `giteaGrantCheck` still refuses any key other than method, agent, rotateBy and target.
- Step 5: the upstream set stays for forks. A repository is accepted when it is the user's fork of a
non-library publish upstream (today's rule), or when its `full_name` is a library publish project's
`owner/repo` and its `permissions` are exactly `{admin: false, push: level === 'write', pull: true}`. Any
other repository, a wider permission or `admin: true` refuses `gitea-reach-wider` with the repository
named.
- `#giteaRole`: the `CREATIVE_DEFINITIONS` refusal becomes "unless every project the raise names is a
library project". `#giteaRole` gets the raise's project list from its two callers (the binding at
:2168 and the recheck at :2201), so it can decide. The comment cites mos-1320 item 2 and keeps
mos-1163 for every other project and every host grant. The host refusals at :1106 and :1254 and their
tests stay as they are.
- `#giteaTargets` returns `{project, upstream, library: true, repository, level}` for a library project and
today's `{project, upstream, fork}` otherwise. `forkProblem` still runs for both: a library remote on
another host refuses `remote-host-mismatch`, and one the seat user owns refuses `remote-not-fork`.
- The block: a library target prints `Repository: <remote>, level <write|read>.` in place of its fork line,
plus the brief's line "The token can write every path of this repository. Only the bus's path check
limits it to supporting folders." for a `write` level. For a `read` level the line says the token can read
every path. (The brief gives only the write line; Dewey can rule the read wording.)
- Revoke deletes the token as today; the collaborator entry stays, as forks do. A fire sets
`prohibit_login` as today. Neither path changes.
- `scripts/case2-install.sh` (row 226's file): the chief of staff's instance gets `identity` and
`publish: ["books"]`. If `#grantableAgent` refuses the chief as its own target, the row stops and goes to
Sage. Read of :2037-2042 says it passes when the chief is `launch.by`; a test proves it.
## 3. Tests (proposed)
Docker (`gitea-grant-docker.test.mjs`), each fact first, before any code is trusted:
- F-A: the admin token with `ADMIN_SCOPES` adds a collaborator to a repository the admin account doesn't
own. F-B: `/user/repos` with the seat token lists that repository with the granted `permissions`. Either
failing stops the row; the output goes to Mos.
- F-C (recorded, not gating): a protected file pattern on her branch refuses a seat user's push to a story
path while her own push passes. F-D (recorded): branch protection refuses a seat user's force-push while a
normal push passes. Each logs its result as a diagnostic and doesn't fail the file; BUILD.md and a message
to Mos carry them.
Fake API (`gitea-grant.test.mjs`, `fake-gitea-api.mjs`): collaborators carry a permission, `repoJson`
returns `permissions`, and the PUT route requires a site admin token with `write:repository`.
- L1 a creative definition gets `write`; L2 the chief gets `read`, including a mixed raise of the library
project and a forked one (Q3).
- L4 a repository with `push: true` for a read grant, or `admin: true`, or a fork of her library repository,
refuses `gitea-reach-wider` naming it.
- L5 revoke deletes the token and leaves the collaborator.
- L6 a non-library project still forks, and its `grant.applied` body is byte-identical to today's.
- L7 a creative raise naming a non-library project, or a library and a non-library one, refuses
`gitea-not-declared`.
- L8 the block holds the repository, the level and the brief's line, plus B-a to B-c (dewey-0862); the notice
has no "forks: ." on a library-only grant.
- L10 a project flipped to `kind: "library"` between the raise and the recheck gives `decision-mismatch`.
- L9 the chief raises its own grant through `credential.mint`.
- Install test: the business file gives the chief `identity` and `publish`.
Business (`business.test.mjs`, sage-2916): B-1 a creative instance with identity, a gitea credential and a
library project in `publish` loads; B-2 so does the chief; B-3 any other instance with a library project in
`publish` refuses with the new message; B-4 a creative instance or the chief without a gitea credential
still refuses under the existing `publish needs identity and a gitea definition` rule. Mutant: M5 drop the
creative-or-chief condition (any instance loads), killed by B-3.
Mutants: M1 drop the library-only condition from `#giteaRole` (the brief's), killed by L7. Others to list
once the code exists: the level forced to `write` (L2), step 5's exact-permission check loosened to
`push || pull` (L4), the fork path taken for a library project (L1, a fork call counted).
## 4. Gate plan (draft)
On Sage's CLEAR, with TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`, stopping at the first red:
the touched bus test files whole, then the mutants, the bus suite, the Docker test (checking for `^skip`
first, since a missing image skips it), the install test, every `scripts/test-*.sh` under row 281's limits,
then the token grep over the saved output. Timeouts and limits go in this file before the run.
## Runs
Filled in after the runs.