|
42 | 42 | * a login as a seat any more. |
43 | 43 | * |
44 | 44 | * A seat is spelled `LANE#N` — `domain:ui#4`, `repo:cloud#1`, `triage`, |
45 | | - * `epic:#27293#1` — its lane one of the six a seat post's title names |
46 | | - * (`domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`) or the epic lane |
| 45 | + * `epic:#27293#1` — its lane one of the seven a seat post's title names |
| 46 | + * (`domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, `qa`) or the epic lane |
47 | 47 | * `epic:#<parent>`, the seat signature's spelling for the PM delegated a |
48 | 48 | * `pm:epic` parent card (pm-dispatch `seat-lifecycle.md` 〈Epic 子树车道〉), so a |
49 | 49 | * login never reads as a seat, and an absent number read as seat 1 |
|
52 | 52 | * because a ref cannot carry `:` or `#`: the default `domain:` kind dropped, |
53 | 53 | * every other `:` (with the epic lane's `#` after it) and the seat's `#` turned |
54 | 54 | * into `-` (`ui-4`, `repo-cloud-1`, `triage-1`, `security-1`, `review-1`, `epic-27293-1`). No `domain:` |
55 | | - * lane may be named `triage`, `director`, `security`, `review`, `repo-…` or `epic-…` — `seatKey` |
| 55 | + * lane may be named `triage`, `director`, `security`, `review`, `qa`, `repo-…` or `epic-…` — `seatKey` |
56 | 56 | * refuses one — which keeps the slug one-to-one. |
57 | 57 | * |
58 | 58 | * ## What it does, in order (open) |
@@ -171,17 +171,17 @@ const REPO_SHAPE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; |
171 | 171 | const LOGIN_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; |
172 | 172 | const BRANCH_SHAPE = /^(?!\/|.*\/\/|.*\.\.|.*[\s~^:?*[\\]|.*\.lock$|.*\/$)[A-Za-z0-9._/-]+$/; |
173 | 173 | const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/; |
174 | | -// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, the six a seat post's title |
175 | | -// names, or the epic lane `epic:#<parent>` — then `#N`; absent = seat 1. Read lower-cased. The bare lanes are a closed |
176 | | -// set of four, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is |
| 174 | +// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, `qa`, the seven a seat post's |
| 175 | +// title names, or the epic lane `epic:#<parent>` — then `#N`; absent = seat 1. Read lower-cased. The bare lanes are a |
| 176 | +// closed set of five, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is |
177 | 177 | // built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same |
178 | | -// four words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this |
| 178 | +// five words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this |
179 | 179 | // list cannot import (the patrol loads this file lazily, so that edge would be a cycle); fleet-roster's self-test, |
180 | 180 | // which imports both, holds the two equal, `coordinator` (it signs and has no mailbox) aside. |
181 | 181 | /** The lane kinds that carry a name after the colon: `domain:<x>`, `repo:<x>`. `domain` is the default kind the slug drops. */ |
182 | 182 | export const SEAT_LANE_KINDS = Object.freeze(['domain', 'repo']); |
183 | | -/** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`), `review` (the contract-review seat, `lanes/review.md`). */ |
184 | | -export const SEAT_BARE_LANES = Object.freeze(['triage', 'director', 'security', 'review']); |
| 183 | +/** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`), `review` (the contract-review seat, `lanes/review.md`), `qa` (the QA routines seat, `lanes/qa.md`). */ |
| 184 | +export const SEAT_BARE_LANES = Object.freeze(['triage', 'director', 'security', 'review', 'qa']); |
185 | 185 | /** The epic lane's kind: `epic:#<parent>` names the `pm:epic` parent card's number, never a word (`epic:#27293#1`). */ |
186 | 186 | export const SEAT_EPIC_KIND = 'epic'; |
187 | 187 | const SEAT_SHAPE = new RegExp(`^((?:${SEAT_LANE_KINDS.join('|')}):[a-z0-9._-]+|${SEAT_EPIC_KIND}:#[1-9][0-9]*|${SEAT_BARE_LANES.join('|')})(?:#([1-9][0-9]*))?$`); |
@@ -571,9 +571,9 @@ async function selfTest() { |
571 | 571 | t('⛔ a `domain:` lane named `epic-…` is refused — its slug would be an epic seat\'s — while `domain:epic` and `domain:epics` read', [seatKey('domain:epic-12#1'), seatKey('domain:epic-tools'), seatKey('domain:epic').slug, seatKey('domain:epics').slug], [null, null, 'epic-1', 'epics-1']); |
572 | 572 | t('⭐ the epic slug collides with no lane slug: one key per slug across every lane kind', (() => { const keys = ['epic:#12#1', 'domain:epic#12', 'domain:epic-12#1', 'repo:epic-12#1', 'domain:12#1', 'triage#12', 'epic:#1#21', 'epic:#12#1'].map(seatKey).filter(Boolean); return new Set(keys.map((k) => k.slug)).size === new Set(keys.map((k) => k.key)).size; })(), true); |
573 | 573 | t('⛔ a short form is not a seat — `skills#2` names no lane, so it is refused as a login is', ['skills#2', 'ui#4', 'server', 'skills'].map(seatKey), [null, null, null, null]); |
574 | | - t('⛔ a `domain:` lane named after a reserved lane word is refused — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1', 'Domain:Triage#1', 'domain:security#1', 'domain:review#1'].map(seatKey), [null, null, null, null, null, null]); |
| 574 | + t('⛔ a `domain:` lane named after a reserved lane word is refused — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1', 'Domain:Triage#1', 'domain:security#1', 'domain:review#1', 'domain:qa#1'].map(seatKey), [null, null, null, null, null, null, null]); |
575 | 575 | t('…while a `domain:` lane that merely CONTAINS a reserved word, and the reserved words as their own lanes, still read', [seatKey('domain:triage-tools#1').slug, seatKey('domain:directory').slug, seatKey('domain:repository').slug, seatKey('triage#2').slug, seatKey('repo:cloud').slug], ['triage-tools-1', 'directory-1', 'repository-1', 'triage-2', 'repo-cloud-1']); |
576 | | - t('the shape and the reserved set are built from the ONE set of lane-word lists', SEAT_LANE_KINDS.join() === 'domain,repo' && SEAT_BARE_LANES.join() === 'triage,director,security,review' && SEAT_EPIC_KIND === 'epic' && SEAT_BARE_LANES.every((w) => seatKey(w)?.key === `${w}#1` && seatKey(`domain:${w}`) === null) && seatKey(`${SEAT_EPIC_KIND}:#7`)?.key === 'epic:#7#1' && seatKey(`domain:${SEAT_EPIC_KIND}-7`) === null, true); |
| 576 | + t('the shape and the reserved set are built from the ONE set of lane-word lists', SEAT_LANE_KINDS.join() === 'domain,repo' && SEAT_BARE_LANES.join() === 'triage,director,security,review,qa' && SEAT_EPIC_KIND === 'epic' && SEAT_BARE_LANES.every((w) => seatKey(w)?.key === `${w}#1` && seatKey(`domain:${w}`) === null) && seatKey(`${SEAT_EPIC_KIND}:#7`)?.key === 'epic:#7#1' && seatKey(`domain:${SEAT_EPIC_KIND}-7`) === null, true); |
577 | 577 | t('the branch is mailbox/<login>/<slug> — no `:` and no `#` in a ref', mailboxBranch('os-bill', seatKey('domain:ui#4')), 'mailbox/os-bill/ui-4'); |
578 | 578 | t('an epic seat\'s branch is mailbox/<login>/epic-N-K', mailboxBranch('sunhaolin', seatKey('epic:#27293#1')), 'mailbox/sunhaolin/epic-27293-1'); |
579 | 579 | t('⭐ two seats under ONE login get two branches', [mailboxBranch('os-sales', seatKey('domain:server#1')), mailboxBranch('os-sales', seatKey('domain:ui#1'))], ['mailbox/os-sales/server-1', 'mailbox/os-sales/ui-1']); |
|
0 commit comments