morgana: row 204 (193a, #1702) viewer level on R1's candidate 44e36fc4c: LEVELS collaborator and viewer, R1's level tests follow, packet updated

This commit is contained in:
2026-10-20 15:04:19 -05:00
parent 1aa9ca9731
commit e9ce59cc43
3 changed files with 31 additions and 28 deletions
+23 -20
View File
@@ -1,8 +1,9 @@
# Row 193a build: the viewer level on R1's access file
Morgana (Opus 5.5), 2026-10-20. Gitea #1691, parent row 193, design `agents/morgana/work/queue-193/DESIGN.md`
r5 (e2d161e2e on morgana/row193, approved at r4 by Duckworth, duckworth-0014). Go: sage-1373 item 3. Base:
`refactor` at aa1bd209e. Lands after FED R1 and is rebased onto R1's landing.
Morgana (Opus 5.5), 2026-10-20. Queue row 204, Gitea #1702 (parent row 193, #1691), design
`agents/morgana/work/queue-193/DESIGN.md` r5 (e2d161e2e on morgana/row193, approved by Duckworth,
duckworth-0018). Go: sage-1373 item 3. Chains on row 201 (FED R1) round 1 candidate gizmoduck/fed-r1 at
44e36fc4c (sage-1412, Q177). Lands after row 201 and rebases onto its landing.
## What it is
@@ -11,21 +12,21 @@ Design 4.2: R1 refuses any grant level but `collaborator`. Row 193a widens that
with the one asked for, so a viewer grant matches `{}` and `{level: 'viewer'}`, and fails
`{level: 'collaborator'}`.
## Why the candidate holds only a test now
## The change
R1 (`packages/business/src/access.mjs`) has no candidate yet. Sage ruled no stub on refactor (sage-1373
item 3). So this branch carries:
On R1's candidate (44e36fc4c):
- `packages/business/tests/access-levels.test.mjs`: the row 193a tests, written against R1's interface as
FED r4.5 (acd5f1f42) sections 3.5 and 7 give it.
- The source change, below, to apply on R1's candidate at rebase.
- `packages/business/src/access.mjs`: `LEVELS` becomes `["collaborator", "viewer"]`. That is the one
source line. R1's level check at :136 already names `grants[<i>].level`, so nothing else moves. Its
comment now says what a viewer is.
- `packages/business/tests/access.test.mjs` (R1's): three places pinned collaborator only. The `LEVELS`
test now expects both levels. The refused-level test drops `viewer` from its bad list, adds `Viewer`,
and both its regexes read `must be one of collaborator, viewer`. R1's `covers` test at :272
(a collaborator grant fails `{level: 'viewer'}`) is unchanged and still holds.
- `packages/business/tests/access-levels.test.mjs` (new): the row 193a tests.
Until R1 lands, the test file fails with `ERR_MODULE_NOT_FOUND` for `../src/access.mjs`. That is expected.
## The source change for the rebase
In R1's level check, the allowed set becomes `["collaborator", "viewer"]`. It is one line wherever R1 keeps
the level rule. Its refusal message must name the key path `grants[<i>].level`, as R1's own rule does.
No other test or script reads either source or test path (grep over `packages/*/tests` and `scripts/`).
R1's own mutant M32 (`if (false) refuse(`) still applies to the same line.
## R1's interface (confirmed, gizmoduck-0029)
@@ -45,11 +46,13 @@ A grant with no `level` refuses (`undefined` in the refused list), which R1 also
## Evidence
Run on 2026-10-20 in the row193a worktree with `node --test --test-reporter=tap
tests/access-levels.test.mjs` from `packages/business`.
Run on 2026-10-20 in the row193a worktree with `node --test --test-reporter=tap <file>` from
`packages/business`, each file alone:
- Base (no access.mjs): fails to load, `ERR_MODULE_NOT_FOUND`.
- Throwaway reference: an `access.mjs` with only the level rule, a declared-business rule and FED's
- On R1's candidate with the change: `access-levels.test.mjs` tests 4, pass 4, fail 0;
`access.test.mjs` tests 24, pass 24, fail 0.
- Before R1 (on aa1bd209e): the new file fails to load, `ERR_MODULE_NOT_FOUND`.
- Throwaway reference, before R1: an `access.mjs` with only the level rule, a declared-business rule and FED's
`covers`, placed in `src/`, run, then deleted, never committed. Result: pass 4, fail 0.
Mutants on that reference, each failing exactly one test:
@@ -61,4 +64,4 @@ Mutants on that reference, each failing exactly one test:
| `covers` ignores the asked level | 3, "covers tells a viewer grant from a collaborator grant" |
| `covers` passes any grant when collaborator is asked | 3 |
At rebase, the same four mutants run again on R1's real code before the row goes to review.
The same four mutants on R1's real code need Sage's slot (mutant batch). Results go here before review.
+2 -2
View File
@@ -10,8 +10,8 @@ import { refuse } from "./errors.mjs";
import { configDir } from "./business.mjs";
import { isPlainObject, requireObject, rejectUnknownKeys, requireId, requirePositiveInt, readJsonFile, deepFreeze } from "./util.mjs";
// The grant levels. Row 193a widens this to collaborator and viewer.
export const LEVELS = Object.freeze(["collaborator"]);
// The grant levels (row 193a, #1691): a viewer reads threads shared with it and drives no instance.
export const LEVELS = Object.freeze(["collaborator", "viewer"]);
// Ids a user may never take: the bus's own principals.
const RESERVED_USERS = Object.freeze(["human", "tester"]);
const LOCAL_HOME = "local";
+6 -6
View File
@@ -70,8 +70,8 @@ test("checkAccess does not freeze or alias the caller's document", () => {
assert.notEqual(checked.grants, d.grants);
});
test("LEVELS is the single allowed level set, collaborator only", () => {
assert.deepEqual([...LEVELS], ["collaborator"]);
test("LEVELS is the single allowed level set, collaborator and viewer (row 193a)", () => {
assert.deepEqual([...LEVELS], ["collaborator", "viewer"]);
assert.ok(Object.isFrozen(LEVELS));
});
@@ -168,11 +168,11 @@ test("grants: instances is [\"*\"] or distinct declared instances of that busine
assert.ok(checkAccess(mutated((d) => { d.grants[0].instances = ["researcher", "writer"]; }), context()));
});
test("grants: any level other than collaborator refuses, naming grants[i].level", () => {
for (const bad of ["viewer", "owner", "Collaborator", "COLLABORATOR", "", null, 1]) {
refuses(() => checkAccess(mutated((d) => { d.grants[1].level = bad; }), context()), /grants\[1\]\.level must be one of collaborator/);
test("grants: any level other than collaborator or viewer refuses, naming grants[i].level", () => {
for (const bad of ["owner", "Collaborator", "COLLABORATOR", "Viewer", "", null, 1]) {
refuses(() => checkAccess(mutated((d) => { d.grants[1].level = bad; }), context()), /grants\[1\]\.level must be one of collaborator, viewer/);
}
refuses(() => checkAccess(mutated((d) => { delete d.grants[0].level; }), context()), /grants\[0\]\.level must be one of collaborator \(got undefined\)/);
refuses(() => checkAccess(mutated((d) => { delete d.grants[0].level; }), context()), /grants\[0\]\.level must be one of collaborator, viewer \(got undefined\)/);
});
test("grants: expires is optional ISO time, and a past expiry is not refused", () => {