Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b107116036 | ||
|
|
d16958bcfd | ||
|
|
48f1627be2 | ||
|
|
17f056787f | ||
|
|
200dd396c4 | ||
|
|
537902d261 | ||
|
|
f58049de02 | ||
|
|
33f49f7d5a | ||
|
|
4a4bef6c13 |
No files matched your search
@@ -5457,3 +5457,21 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-24 | Goldie | Row325 / #1821 before (mos-1563/1564) | Real .mosaic/project.json idstack disagrees with Case1 project's mosaic-stack key; row323 scratch fixture omitted project file and only warned. New goldie/row325 at99930362e changes only that JSON key and expressly authorized old assertion key; new real-byte project-copy validation tests require valid and reject old key. Mutant prepared; no tests before Sage CLEAR, no queue writes/host actions.
|
||||
|
||||
2026-10-24 | Goldie | Row325 / #1821 after (sage-3882 CLEAR) | Only Case1 projects key becomesstack; only old test70 expected key updated under mos-1564. New3-case test copies real .mosaic/project.json bytes, requires CLI project valid, rejects old key. Serialized shortTMP timeout-k10: new3/3, business264/264, old-key mutant killed/restored SHA9e43bfef, restored new3/3; all positive0 fail/skip/cancel. Manifest4/4 verifies including unchanged project document. Logs/load lines and mutant diff in BUILD.md; no queue writes/host action, Gladstone review/Sage gate pending.
|
||||
|
||||
2026-10-24 — Poe, row 208 / 193d (#1706), before: building Console shared list,
|
||||
share controls and per-grant viewer read-only against approved design e2d161e2e.
|
||||
Base ec86f2c84. Isolated tests and UI-guard mutants pending Sage CLEAR; no live action.
|
||||
|
||||
2026-10-24 — Poe, row 208 / 193d (#1706), after: Console shared list, share/unshare
|
||||
controls and live per-grant viewer UI implemented. Webui 418/418 and plugins
|
||||
119/119 green under sage-3925 CLEAR; four UI guard mutants killed with viewer
|
||||
bus bypass refusals. Prior old-test draft regression fixed under sage-3918;
|
||||
missing root oauth4webapi corrected by pinned npm ci under sage-3924. Evidence:
|
||||
agents/poe/work/queue-208/BUILD.md. Independent reviews and Sage's global gate
|
||||
remain; no live state, schema or bus policy change.
|
||||
|
||||
2026-10-24 | Goldie | Row265 / #1770 before, recorded during authorized verification | C2-09b has no project room verbs. On goldie/row265 at ec86f2c84, add only room.open/send/read, pure exported leading-tag parser, README verb rows and new bus tests. Sage-3921 fixes EOF-tag and message-only timestamp readings; tester list/helpers/schema stay unchanged. Claim rev3814; Sage-3932 CLEAR for serialized new files, whole bus and twelve refusal mutants. No old-test edits authorized; stop rules apply.
|
||||
|
||||
2026-10-24 | Goldie | Row265 / #1770 first slot STOP (sage-3932) | New parser14/14, rooms18/18 green; serialized bus stops at old project-runtime.test.mjs:14 import-closure pin, partial619 ok/1 not ok. New pure address.mjs import is absent from expected imports. Static trace also needs address.mjs in seat RUNNER_FILES. No old-test/product correction, no mutant/restored run, no green commit/push; old-pin and out-of-owned-files mount change sent to Sage for ruling. Audit prose missed the direct source pin despite raw grep hit; correction recorded in BUILD.md.
|
||||
|
||||
2026-10-24 | Goldie | Row265 / #1770 verified (mos-1567, sage-3942) | Approved correction adds solely address import pin + Row265 comment to project-runtime.test and one pure-source RUNNER_FILES mount; new block checks parser has no imports. Serialized fresh-short-TMP timeout-k10: unchanged sandbox10/10, parser15/15, rooms18/18, full bus957/957, twelve named refusal mutants killed (each Node1, driver0), restored parser15/15 + rooms18/18. All positives zero fail/skip/cancel; every mutation restored full broker SHA71b32ee4. Old first-red evidence retained, no other old-test change. Slot returned goldie-0034; Boyd packet explicitly includes mount-list review, candidate push next.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Row 208 (193d) second review: poe/row208 4a4bef6c1 (duckworth-0302)
|
||||
|
||||
Request: sage-3934 (sage-3931 request, c31492 on #1706). Candidate: 4a4bef6c1 on ec86f2c84, one
|
||||
commit. Design: e2d161e2e:agents/morgana/work/queue-193/DESIGN.md section 8, sha256 dd7fcb41
|
||||
(matches). Scope under mos-1542: the guards only. The question is whether a viewer can widen
|
||||
authority.
|
||||
Method: static. Git reads, plus the mutant summary Sage sent by path. No node, no tests.
|
||||
|
||||
## Verdict: APPROVE (static)
|
||||
|
||||
The diff has no bus or schema change. Every write is still decided by the broker as it stood at the
|
||||
base. The webui adds routes and hints, and no authority.
|
||||
|
||||
- **chat-http.mjs.**
|
||||
- share and unshare are POST only. They come after the same write fences as every other write:
|
||||
send and named mode, the SSO session or auth header, Origin, and sec-fetch-site.
|
||||
- The body must be exactly `{to}`: a string of 1-200 characters with no CR, LF or NUL. Extra keys
|
||||
(owner, verb) refuse 400, and the test covers that.
|
||||
- shares is GET only and takes no query.
|
||||
- The reply must echo thread, to and op exactly. Anything else is 503 outcome-unknown.
|
||||
- The new STATUS codes map share-not-covered to 403, share-exists to 409 and share-not-found to 404.
|
||||
- `?shared=` accepts only `true`.
|
||||
- **chat.mjs and the adapter.**
|
||||
- The three verbs go over `createChannelCaller` to the webui adapter (index.mjs:12-14). The adapter
|
||||
is an allowlist passthrough to the session's own `call`.
|
||||
- A granted user's chat is rebound per session (serve.mjs:701), so share calls carry the user's
|
||||
principal, never the owner's.
|
||||
- The test fixture runs the real adapter and the real Broker.
|
||||
- **The broker decides each new path, unchanged from the base.**
|
||||
- thread.share, thread.unshare and thread.shares are in USER_VERBS and not in VIEWER_VERBS, so
|
||||
userLevel gives collaborator. A viewer gets access-refused at #grantedThread (broker.mjs:950).
|
||||
- Each of the three goes through `#thread`'s owner check (broker.mjs:909-913). A sharee gets
|
||||
thread-not-owned and can't reshare.
|
||||
- share-not-covered applies to a `user:` target (broker.mjs:6559).
|
||||
- Only thread.messages takes `#threadRead`, so a share gives read access and nothing else.
|
||||
- **The grant-hint predicate.**
|
||||
- access.mjs `chatGrants` re-runs `lookup`, so the subject must still sign in as session.user with a
|
||||
live grant. It then keeps only that user's grants in this business that cover at collaborator,
|
||||
which is the same `covers` call as the broker's `#granted`.
|
||||
- It returns only `{projects, instances}`. No ids, receipts, subjects or other users escape. Any
|
||||
failure gives [].
|
||||
- The client's `scopeWrites` requires the project and the instance to match within one grant
|
||||
(`some(g => project && instance)`). It never pairs a project from one grant with an instance from
|
||||
another, so mixed grants can't combine into a scope that no single grant has.
|
||||
- `includes('*')` on the client is equivalent to covers' `[0] === '*'`, because checkScope
|
||||
(business access.mjs:97-103) refuses `*` unless it is alone.
|
||||
- The hint is never carried over: chatGrants starts at [] and is re-read from /api/config on each
|
||||
user render. It is in /api/config only when `granted`.
|
||||
- **No client-only hiding stands in for a broker refusal.** Each hidden control maps to a refusal:
|
||||
|
||||
| Hidden control | Who | Broker refusal |
|
||||
| --- | --- | --- |
|
||||
| composer and resume | viewer | thread.send/open: access-refused |
|
||||
| composer and resume | sharee | thread.send/open: thread-not-owned |
|
||||
| share form | viewer | access-refused |
|
||||
| share form | sharee | thread-not-owned |
|
||||
| create thread | out of scope | collaborator check at create |
|
||||
| create project | user | project.create isn't in USER_VERBS |
|
||||
| session controls, clear, cancel | user | session.* isn't in USER_VERBS (tester-only) |
|
||||
|
||||
Test :136 POSTs create, send, open and share from the page with the UI bypassed, and checks four
|
||||
403 access-refused responses, unchanged table counts, and that each verb reached the bus. Test :19
|
||||
adds unshare and the shares read over HTTP. Test :154 covers the sharee's thread-not-owned on send,
|
||||
open and share.
|
||||
- **Mutants, as reported (sage-3920 CLEAR, /mnt/storage/scratch/tmp/8UlAhS/poe-208-mutants.json, not
|
||||
rerun).**
|
||||
- There are four served-copy mutants: composer, open, share and create. Each one's anchor must exist,
|
||||
or the fixture throws.
|
||||
- Each mutant fails the UI-absence test with ERR_ASSERTION, and the bypass test passes under each
|
||||
one, so the broker still refuses. That meets design section 8: "With each UI check removed, the
|
||||
bus still refuses."
|
||||
- The full webui suite passed 418/0 and plugins 119/0 (sage-3925), as reported.
|
||||
|
||||
## Notes
|
||||
|
||||
- N1 (outside guard scope). user.js:16 re-renders every 5 s while the tab is visible. Each render
|
||||
re-reads /api/config, projects, threads and the shared list. This doesn't affect authority, but it
|
||||
is steady load per open user tab.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Glomgold
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under Q246 and mos-1571. Owns row 309, O2 W1: the reviewed local
|
||||
thesaurus data (docs/plans/2026-10-21_q195-writing-editor.md, "W1 reviewed local thesaurus data").
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `glomgold/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and give the Opus 5.5 landing review (mos-1571). Registered by
|
||||
Sage on 2026-10-24 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,323 @@
|
||||
# Row 309 / O2 W1 / Gitea #1823
|
||||
|
||||
2026-10-24 — Glomgold (d17504fd-2f56-4d9f-bff9-eabb450f8a94).
|
||||
Base: d16958bcf; branch glomgold/row309. Read AGENTS.md, seat README,
|
||||
W1, D2, D22, Proposed rows, G1 and CURRENT.md. Approved assignment provides
|
||||
scope and acceptance; no runtime integration or new design approval required.
|
||||
|
||||
## Intended build and acceptance
|
||||
|
||||
Retain all 18 original WordNet files as text, preserving Princeton LICENSE
|
||||
bytes; add NOTICE and per-file provenance. Keep the downloaded ZIP outside git.
|
||||
No binary source archive needs to land. Implement a deterministic offline
|
||||
importer/converter with source integrity, strict records and count checks.
|
||||
Ship JSON lookup data plus license/notice under src/public/thesaurus, not source
|
||||
or converter code. Prefer explicit sense identifiers, glosses, lemmas, index
|
||||
and exceptions over a lossy synonym-only table. Record deterministic output
|
||||
hashes and bytes; measure JSON parse heap under Node with explicit GC, labeling
|
||||
it as Node rather than browser evidence. All tests and measurements await Sage
|
||||
CLEAR, followed by Boyd review and a single Duckworth landing review. No runtime
|
||||
fetch, UI, route, worker, CSP change, live stack or private books.
|
||||
|
||||
## Source intake — blocked on count mismatch
|
||||
|
||||
Fetched only the assigned source:
|
||||
https://raw.githubusercontent.com/nltk/nltk_data/96f9b3252457a2b97e52aec64c3dfceeb5c312d5/packages/corpora/wordnet.zip
|
||||
|
||||
Archive retained outside repository:
|
||||
/mnt/storage/scratch/tmp/glomgold-wordnet-3.0.zip
|
||||
|
||||
- Archive bytes: 10,775,600 — matches.
|
||||
- Archive SHA-256: cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59 — matches.
|
||||
- Regular entries: 18; uncompressed total: 36,353,991 — matches.
|
||||
- data.*, index.* and *.exc total: 35,435,978 — matches.
|
||||
- Synsets: 117,659 — matches.
|
||||
- Distinct literal lemma strings in data.*: 149,229.
|
||||
- Distinct lowercased lemmas after removing adjective (a)/(p)/(ip) suffixes: 147,306.
|
||||
- Distinct first-field strings across index.noun/index.verb/index.adj/index.adv:
|
||||
147,306. This is 28 above D22's cited Princeton statistic of 147,278.
|
||||
|
||||
Counting method: read the verified ZIP using Python standard-library zipfile;
|
||||
exclude header lines beginning with two spaces. Each remaining data.* line is
|
||||
one synset; field 4 is the hexadecimal word count and following alternating
|
||||
word/lex_id fields supply its lemmas. Independently take the first whitespace
|
||||
field of each nonheader index.* line into a union set across the four files.
|
||||
Both normalized data and index counts agree at 147,306. This was source intake,
|
||||
not a test-suite run. No converter, generated data, tests or heap measurement
|
||||
has run or been written. Per explicit assignment, stop on differing count and
|
||||
ask Sage to resolve the expected count before proceeding. No alternate source.
|
||||
|
||||
Queue next from the linked worktree refused because queue operations require
|
||||
the canonical checkout. Retried there and received row 309. Claim operation
|
||||
is glomgold-0001, initially still pending; do not repeat with a fresh key.
|
||||
|
||||
Claim completed: `ok glomgold-0001 rev 3856 row 309 briefed→in-progress`.
|
||||
Sage notified through T3, glomgold-0002 at 05:38:57Z. No commit/push.
|
||||
Session and BUILD-LOG registry entries require Sage coordination because those
|
||||
shared files are outside the assignment's exclusive owned-file list. Proposed
|
||||
session line: 2026-10-24 | glomgold | row 309 / #1823 W1 source intake |
|
||||
blocked: pinned archive has 147,306 distinct headwords versus cited 147,278;
|
||||
archive bytes/hash and synsets match; no tests, commit or push.
|
||||
|
||||
## Sage ruling sage-3972 (05:40:30Z)
|
||||
|
||||
Proceed with the exact pinned mirror. Pin 147,306 distinct headwords, 117,659
|
||||
synsets and the source hashes. Record literal data lemmas =149,229. The 147,278
|
||||
is the cited Princeton page figure, not an expected count measured on this ZIP.
|
||||
The 28-string gap remains open for review and is not a build blocker.
|
||||
|
||||
Bounded investigation: the Princeton statistics page reports per-POS strings
|
||||
117,798 noun; 11,529 verb; 21,479 adjective; 4,481 adverb, total 155,287.
|
||||
All four match the pinned mirror. The page explicitly states its 147,278 is
|
||||
the cross-POS union, so summing per-POS counts does not explain the gap.
|
||||
Mirror index lowercasing, replacing underscores with spaces, and collapsing
|
||||
repeated underscores each leave 147,306. Removing underscores entirely gives
|
||||
146,449, not the cited count. Data normalization independently gives 147,306;
|
||||
case/marker differences explain literal 149,229, not the remaining 28-string
|
||||
gap. No proven explanation found; no source substitution or extra upstream
|
||||
archive comparison. Investigation ended after these bounded checks.
|
||||
References: https://wordnet.princeton.edu/documentation/wnstats7wn and
|
||||
https://wordnet.princeton.edu/documentation/wndb5wn (format description).
|
||||
|
||||
Implementation choice: one JSON shard per POS, retaining glosses and full
|
||||
lemma spelling (including markers), lowercase ordered sense index and exception
|
||||
lists. This lets a future consumer release unused POS objects. JSON is data,
|
||||
with no script, worker, UI or route. Consumer hints (max senses/synonyms) are
|
||||
explicit tunable defaults and never truncate the dataset at build time.
|
||||
The importer takes an explicit local ZIP, validates its bytes/hash before
|
||||
Python stdlib extraction, and never downloads. Normal converter builds use
|
||||
only the verified retained text; no archive or Python needed for regeneration.
|
||||
Tests authored before converter; first run will follow implementation under
|
||||
Sage's explicit code-then-CLEAR workflow rather than unslotted red runs.
|
||||
|
||||
Code and data tests written; no test/build/heap run yet. Proposed CLEAR work:
|
||||
- Node syntax checks for both new converters and the new test.
|
||||
- Offline archive import to scratch and generation into shipped public path.
|
||||
- Independent thesaurus-data.test.mjs, with THESAURUS_ARCHIVE naming the verified
|
||||
external ZIP (round-trip coverage), two full conversions and shipped-byte checks.
|
||||
- Fresh-process --expose-gc measurement three times, recorded here as plain text.
|
||||
- Full affected WebUI suite serially, preserving any existing failure for Sage.
|
||||
- git diff --check and binary diff review before own-branch commit/push.
|
||||
All commands run with TMPDIR from mktemp /mnt/storage/scratch/tmp/XXXXXX.
|
||||
|
||||
## Verification under CLEAR sage-3975
|
||||
|
||||
Runtime: Node v26.8.1; Python 3.12.8; Linux x64. Converter uses only Node standard
|
||||
library; importer uses only Python stdlib zipfile, invoked over verified stdin.
|
||||
Node syntax checks for both converters and the new test exited 0.
|
||||
First generation refused `index.noun: 'hood`: my index grammar omitted the
|
||||
aggregated domain pointer symbols `;` and `-` used in index files (data files
|
||||
use the expanded variants). Inspected all source index pointer symbols,
|
||||
added those two index-only symbols, regenerated successfully. No source edit
|
||||
or old-test change. Generated evidence: generation.txt.
|
||||
|
||||
Independent test command (fresh scratch TMPDIR, THESAURUS_ARCHIVE set):
|
||||
`node --test packages/webui/tests/thesaurus-data.test.mjs` — 8/8 pass, no skip,
|
||||
no failure. Includes two complete conversions vs shipped bytes and each other,
|
||||
all source/license hashes, malformed records, changed source/provenance,
|
||||
missing license, extra inventory, invalid archive, full verified archive
|
||||
round-trip and overwrite refusal. Log: thesaurus-test.tap.
|
||||
|
||||
Generated data: noun.json 15,617,486; verb.json 2,327,528; adj.json 3,319,398;
|
||||
adv.json 648,351; LICENSE 1,625; NOTICE 649; manifest.json 1,623 bytes.
|
||||
Total shipped: 21,916,660 bytes (plain text, no compressed/binary evidence).
|
||||
The data files alone total 21,912,763 bytes. Suggested consumer limits in
|
||||
manifest.json are maxSenses=8 and maxSynonyms=20, tunable hints; all data ships.
|
||||
|
||||
Fresh Node --expose-gc heap measurements (all four shards retained), each with
|
||||
`uptime` immediately before the command; logs heap-1.txt through heap-3.txt:
|
||||
|
||||
| Run | Uptime display / load 1,5,15 | Baseline heap | Retained delta | Sampled post-parse delta |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| 1 | 00:44:34 / 1.47, 2.21, 2.52 | 4,758,736 | 59,679,616 | 60,374,568 |
|
||||
| 2 | 00:44:34 / 1.47, 2.21, 2.52 | 4,758,984 | 59,679,616 | 60,374,568 |
|
||||
| 3 | 00:44:35 / 1.47, 2.21, 2.52 | 4,758,736 | 59,679,616 | 60,374,568 |
|
||||
|
||||
Uptime is host-local; date -u is used for T3 headers. Measurements occurred
|
||||
while Gyro's nice -n 19 cargo build was allowed concurrently. Retained delta
|
||||
is after explicit GC, with objects kept live and 117,659 synsets checked.
|
||||
The sampled post-parse number is not an instrumented high-water mark.
|
||||
These are measured Node costs, not browser-memory claims. No browser/editor
|
||||
code or actual books are involved.
|
||||
|
||||
Sage-3977 authorized ignored root node_modules symlink to canonical dependencies
|
||||
and npm ci --ignore-scripts --no-audit --no-fund in packages/webui; two packages
|
||||
installed from the existing lockfile. The first symlink command accidentally
|
||||
used webui cwd; npm ci removed that temporary symlink, then the intended root
|
||||
symlink was created. Canonical dependency files were not changed. Both final
|
||||
node_modules paths are ignored and absent from git status --porcelain.
|
||||
Full suite started with MOSAIC_REQUIRE_EDITOR_BUILD=1, concurrency=1 and the
|
||||
verified external THESAURUS_ARCHIVE. No tracked package/lockfile changes.
|
||||
|
||||
Public-directory inventory review: shell.test.mjs enumerates only the font
|
||||
subdirectory; editor-provenance.test.mjs hashes only third_party/editor and
|
||||
specific editor outputs. No test found hashing/enumerating all src/public as
|
||||
a closed file list. The new subdirectory does not enter those inventories.
|
||||
SESSIONS and BUILD-LOG will be handled by Sage at landing (sage-3975).
|
||||
|
||||
## First full suite: missing dependency, superseded by isolated install
|
||||
|
||||
Full run exited 1: 426 tests, 376 pass, 50 fail, 0 skipped; 272657.498313 ms.
|
||||
The summary confirms missing oauth4webapi (ERR_MODULE_NOT_FOUND). The authorized
|
||||
canonical root node_modules symlink pointed at an incomplete installation.
|
||||
No old tests or authentication code changed. Sage-3981 supersedes the base-worktree
|
||||
comparison: remove ONLY my root symlink and npm ci from the root lockfile into
|
||||
an isolated worktree root node_modules, then rerun decide-sso/granted-user once.
|
||||
Only if both pass, run the full suite again. No baseline worktree was created.
|
||||
Canonical node_modules remains untouched. All first-run failure details are
|
||||
retained in webui-suite.tap; the five requested decide-sso stacks follow.
|
||||
|
||||
```text
|
||||
test at packages/webui/tests/decide-sso.test.mjs:79:1
|
||||
✖ a signed-in owner confirms and answers, with the session's provider, issuer and sub as the subject (26.393114ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs
|
||||
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:331:9)
|
||||
at packageResolve (node:internal/modules/esm/resolve:784:25)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:873:18)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32 {
|
||||
code: 'ERR_MODULE_NOT_FOUND'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/decide-sso.test.mjs:99:1
|
||||
✖ the listener gates: no session, a cross-origin page, a loopback Host, a GET, and a sign-out between the steps (31.164949ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs
|
||||
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:331:9)
|
||||
at packageResolve (node:internal/modules/esm/resolve:784:25)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:873:18)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32 {
|
||||
code: 'ERR_MODULE_NOT_FOUND'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/decide-sso.test.mjs:121:1
|
||||
✖ J16: without a decision transport the routes are read-only and /api/config says no; startServer refuses decide without sso or send (24.009216ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /need --sso and --send/. Input:
|
||||
|
||||
"Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs"
|
||||
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/tests/decide-sso.test.mjs:124:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs
|
||||
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:331:9)
|
||||
at packageResolve (node:internal/modules/esm/resolve:784:25)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:873:18)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32,
|
||||
expected: /need --sso and --send/,
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/decide-sso.test.mjs:147:1
|
||||
✖ J19: a granted user's session gets decide false and the user page config, and a decision POST stops at owner-only (49.361897ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs
|
||||
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:331:9)
|
||||
at packageResolve (node:internal/modules/esm/resolve:784:25)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:873:18)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32 {
|
||||
code: 'ERR_MODULE_NOT_FOUND'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/decide-sso.test.mjs:173:1
|
||||
✖ N4: decision.resolve has no Console route, the --send caller names no decision verb, and consoleCall takes only the two console verbs (70.935161ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oauth4webapi' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/glomgold-row309/packages/webui/src/sso.mjs
|
||||
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:331:9)
|
||||
at packageResolve (node:internal/modules/esm/resolve:784:25)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:873:18)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32 {
|
||||
code: 'ERR_MODULE_NOT_FOUND'
|
||||
}
|
||||
```
|
||||
|
||||
## Environment repair and final candidate validation
|
||||
|
||||
Isolated worktree root npm ci installed 128 packages from the unchanged lockfile
|
||||
(root-install.txt); no canonical install, lockfile or source edits.
|
||||
`decide-sso.test.mjs` alone: 5/5 pass (decide-sso-rerun.tap).
|
||||
`granted-user.test.mjs` alone: 19/19 pass (granted-user-rerun.tap).
|
||||
Both used separate fresh scratch TMPDIRs and MOSAIC_REQUIRE_EDITOR_BUILD=1.
|
||||
This confirms missing dependency setup rather than a W1 source regression;
|
||||
no base worktree needed under sage-3981. Full serial suite restarted only after
|
||||
both files passed; log webui-suite-final.tap.
|
||||
|
||||
Final source cleanup before the rerun: BUILD.json now explicitly records
|
||||
Node v26.8.1 and the initial-import Python 3.12.8 in addition to API baselines
|
||||
and input/output hashes. Removed the optional no-op heap-test wrapper: heap
|
||||
measurement is three explicit CLI runs, not a reported test without assertions.
|
||||
Regenerated metadata, syntax-checked all three new modules, reran the dedicated
|
||||
data test independently: 7/7 pass, 0 skip (thesaurus-test-final.tap). Generated
|
||||
data bytes/hashes are unchanged; only build metadata/input hashes changed.
|
||||
|
||||
## Landing hold mos-1573
|
||||
|
||||
Per sage-3980: W1 may build and push only glomgold/row309 for Boyd review.
|
||||
Nothing lands on refactor until Jason decides whether roughly 58 MB of source
|
||||
and derived text should be permanent repository history (Vikunja 618).
|
||||
After a green local commit, measure the exact packed bytes via
|
||||
`git rev-list --objects d16958bcf..HEAD | git pack-objects --stdout | wc -c`
|
||||
and report `git count-objects -v` alongside it. No merge or protected-branch push.
|
||||
|
||||
Whitespace review: authored source/docs/data/evidence pass staged diff --check
|
||||
when excluding only the original wordnet/ tree and its two exact LICENSE
|
||||
copies. The unfiltered check reports 273,271 upstream whitespace findings in
|
||||
13 files (all original WordNet text or LICENSE copies). Their exact bytes are
|
||||
required by W1 and verified against the pinned archive; no normalization was
|
||||
made. Evidence logs/stack excerpts have trailing blank-line spaces trimmed
|
||||
for review only; test content, errors, statuses and counts are preserved.
|
||||
No binary entries appear in staged numstat. The ZIP and the full whitespace
|
||||
checker diagnostic remain outside git.
|
||||
|
||||
## Candidate completion
|
||||
|
||||
Final full affected suite:
|
||||
`MOSAIC_REQUIRE_EDITOR_BUILD=1 THESAURUS_ARCHIVE=/mnt/storage/scratch/tmp/glomgold-wordnet-3.0.zip node --test --test-concurrency=1 packages/webui/tests/`
|
||||
with fresh scratch TMPDIR — 425/425 pass, 0 failures, 0 skips, exit 0;
|
||||
277018.268665 ms (webui-suite-final.tap). Old test files remained byte-identical.
|
||||
This includes the required pinned esbuild provenance rebuild. The dedicated
|
||||
new test independently passed 7/7 before the full run.
|
||||
|
||||
All committed additions are within W1's assigned source/data/test paths plus
|
||||
this explicitly assigned seat work directory. Both node_modules paths are
|
||||
ignored and absent from git status --porcelain. Source ZIP remains outside git.
|
||||
Staged numstat contains no binary files. Authored-path staged diff --check
|
||||
passes; the unmodified upstream whitespace findings are documented above.
|
||||
License/provenance approval awaits Boyd, then Duckworth once on the landing
|
||||
candidate. No refactor merge/push, release or deployment is authorized here.
|
||||
|
||||
Final registry line for Sage at landing: 2026-10-24 | glomgold | row 309 / #1823
|
||||
W1 pinned WordNet data | 147306 headwords / 117659 synsets, deterministic
|
||||
21,916,660-byte shipped data; Node retained heap 59,679,616 bytes (3 runs);
|
||||
independent 7/7 and WebUI 425/425; source/data plain text; history-size hold
|
||||
mos-1573 and independent review remain external gates.
|
||||
@@ -0,0 +1,13 @@
|
||||
✔ a signed-in owner confirms and answers, with the session's provider, issuer and sub as the subject (92.914873ms)
|
||||
✔ the listener gates: no session, a cross-origin page, a loopback Host, a GET, and a sign-out between the steps (36.478745ms)
|
||||
✔ J16: without a decision transport the routes are read-only and /api/config says no; startServer refuses decide without sso or send (206.814095ms)
|
||||
✔ J19: a granted user's session gets decide false and the user page config, and a decision POST stops at owner-only (77.212086ms)
|
||||
✔ N4: decision.resolve has no Console route, the --send caller names no decision verb, and consoleCall takes only the two console verbs (44.65112ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 5
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 564.772519
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"sourceManifestSha256": "e5a799a6693321be71c4e99080b9f89ba12a304c3092d05839bb85355194fe76",
|
||||
"archive": {
|
||||
"bytes": 10775600,
|
||||
"sha256": "cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59"
|
||||
},
|
||||
"counts": {
|
||||
"synsets": 117659,
|
||||
"headwords": 147306,
|
||||
"literalLemmas": 149229
|
||||
},
|
||||
"countMethod": "Union of first-field headwords across four index.* files; cross-checked with lowercased data.* lemmas stripping terminal (a)/(p)/(ip). Literal data lemmas counted without normalization.",
|
||||
"openNote": "Princeton statistics page union 147278 differs by 28; unresolved, accepted in sage-3972.",
|
||||
"tools": {
|
||||
"node": "v26.8.1",
|
||||
"initialImportPython": "3.12.8",
|
||||
"converter": "Node.js >=24, standard library only; JSON UTF-8 LF, no timestamps, no locale sorting",
|
||||
"importer": "Node.js >=24 plus Python 3 stdlib zipfile; local pinned ZIP only"
|
||||
},
|
||||
"inputs": {
|
||||
"editor-data/import-wordnet.mjs": "9b3a668b83d40b5c2659b56c647c40db494d8a9f35fb7ab463b46892ae0d530a",
|
||||
"editor-data/build-thesaurus.mjs": "f1b0b062452fedfb1298f9785f4934046a8b6a0ed8704f5cb82c683d6deb60aa"
|
||||
},
|
||||
"outputs": {
|
||||
"noun.json": {
|
||||
"bytes": 15617486,
|
||||
"sha256": "5294d3f0e6cd7924f5be97d4d6e64b96a9dd8505f216e5c056c3f5b69c0c2dc9"
|
||||
},
|
||||
"verb.json": {
|
||||
"bytes": 2327528,
|
||||
"sha256": "3f99489858d8cbce9996c72135f82325bd2c067a64d2fb8b80dea386b99b6392"
|
||||
},
|
||||
"adj.json": {
|
||||
"bytes": 3319398,
|
||||
"sha256": "9965dbfdc35a11b829fedb19e7b22ab3ca8470f45c5532c1f310810012a23023"
|
||||
},
|
||||
"adv.json": {
|
||||
"bytes": 648351,
|
||||
"sha256": "c6effb7a1502cd3dcd81341cb296e039d238786712c91c30994f2e50ef1e9fcb"
|
||||
},
|
||||
"LICENSE": {
|
||||
"bytes": 1625,
|
||||
"sha256": "7731175a77952e259390b496fab905e57118b8d19ad3a8383c67eee724ff443f"
|
||||
},
|
||||
"NOTICE": {
|
||||
"bytes": 649,
|
||||
"sha256": "508e1a5d132e05ec888a79ec959b68d25203fee0aa3692b0610da7500a7c0251"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 1623,
|
||||
"sha256": "2c58e1513991ea7505ffd8d9dda6e5e8189f8984a7167b051834eb9306e15c72"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
✔ R6 G1 a granted user signs in, gets the user page and reaches the broker as itself (160.658638ms)
|
||||
▶ R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
✔ a listed user with no grant (37.489252ms)
|
||||
✔ a grant in another business (53.430666ms)
|
||||
✔ an expired grant (54.675764ms)
|
||||
✔ another user's grant (73.314097ms)
|
||||
✔ a sub in no file (107.901995ms)
|
||||
✔ a file the loader refuses (52.404505ms)
|
||||
✔ R6 G2 a subject with no live grant in this business is refused subject-unknown (380.127672ms)
|
||||
▶ R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
✔ grant removed (56.085631ms)
|
||||
✔ subject removed (92.518855ms)
|
||||
✔ grant expired (55.61104ms)
|
||||
✔ file now refused (51.75472ms)
|
||||
✔ R6 G3 a revoke counts on the next request: 401 and the session is gone (256.650806ms)
|
||||
✔ R6 G4 every owner route answers 403 owner-only before any bus call (113.040167ms)
|
||||
✔ R6 G5 the bus refuses every owner verb over the user transport on its own (62.938328ms)
|
||||
✔ R6 G9 a user chat sends only the plugins read over the owner transport (103.067067ms)
|
||||
✔ R6 G6 the user page holds the notice word for word (74.913868ms)
|
||||
✔ R6 G7 the hub label and plain peer links render from config on both pages (794.402374ms)
|
||||
✔ R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request (40.148927ms)
|
||||
ℹ tests 19
|
||||
ℹ suites 0
|
||||
ℹ pass 19
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2123.05789
|
||||
@@ -0,0 +1,17 @@
|
||||
00:44:34 up 10 days, 16:04, 2 users, load average: 1.47, 2.21, 2.52
|
||||
{
|
||||
"runtime": "v26.8.1",
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"files": [
|
||||
"noun.json",
|
||||
"verb.json",
|
||||
"adj.json",
|
||||
"adv.json"
|
||||
],
|
||||
"method": "Fresh Node --expose-gc process; GC before/after parsing all four JSON shards; heapUsed delta. parseHeapBytes is a sampled post-parse value, not peak instrumentation; not a browser measurement.",
|
||||
"baselineHeapBytes": 4758736,
|
||||
"retainedHeapBytes": 59679616,
|
||||
"parseHeapBytes": 60374568,
|
||||
"synsets": 117659
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
00:44:34 up 10 days, 16:04, 2 users, load average: 1.47, 2.21, 2.52
|
||||
{
|
||||
"runtime": "v26.8.1",
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"files": [
|
||||
"noun.json",
|
||||
"verb.json",
|
||||
"adj.json",
|
||||
"adv.json"
|
||||
],
|
||||
"method": "Fresh Node --expose-gc process; GC before/after parsing all four JSON shards; heapUsed delta. parseHeapBytes is a sampled post-parse value, not peak instrumentation; not a browser measurement.",
|
||||
"baselineHeapBytes": 4758984,
|
||||
"retainedHeapBytes": 59679616,
|
||||
"parseHeapBytes": 60374568,
|
||||
"synsets": 117659
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
00:44:35 up 10 days, 16:04, 2 users, load average: 1.47, 2.21, 2.52
|
||||
{
|
||||
"runtime": "v26.8.1",
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"files": [
|
||||
"noun.json",
|
||||
"verb.json",
|
||||
"adj.json",
|
||||
"adv.json"
|
||||
],
|
||||
"method": "Fresh Node --expose-gc process; GC before/after parsing all four JSON shards; heapUsed delta. parseHeapBytes is a sampled post-parse value, not peak instrumentation; not a browser measurement.",
|
||||
"baselineHeapBytes": 4758736,
|
||||
"retainedHeapBytes": 59679616,
|
||||
"parseHeapBytes": 60374568,
|
||||
"synsets": 117659
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
npm warn deprecated [email protected]: Use your platform's native DOMException instead
|
||||
|
||||
added 128 packages in 1s
|
||||
@@ -0,0 +1,15 @@
|
||||
✔ pinned source keeps all original files and Princeton license bytes (38.297773ms)
|
||||
✔ two complete conversions match shipped bytes and pinned counts (3418.378387ms)
|
||||
✔ changed source, missing license, changed provenance and extra files refuse before output (68.788374ms)
|
||||
✔ data parser refuses malformed words, offsets, pointers, verb frames and trailing fields (0.365038ms)
|
||||
✔ index parser refuses wrong counts, nonexistent senses and incorrect lemma mapping (0.292268ms)
|
||||
✔ offline importer refuses incorrect archive bytes before extraction (0.416171ms)
|
||||
✔ verified pinned ZIP round-trips to all original text files (366.007687ms)
|
||||
ℹ tests 7
|
||||
ℹ suites 0
|
||||
ℹ pass 7
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3965.261961
|
||||
@@ -0,0 +1,16 @@
|
||||
✔ pinned source keeps all original files and Princeton license bytes (32.957522ms)
|
||||
✔ two complete conversions match shipped bytes and pinned counts (3093.861766ms)
|
||||
✔ changed source, missing license, changed provenance and extra files refuse before output (59.480475ms)
|
||||
✔ data parser refuses malformed words, offsets, pointers, verb frames and trailing fields (0.334357ms)
|
||||
✔ index parser refuses wrong counts, nonexistent senses and incorrect lemma mapping (0.233577ms)
|
||||
✔ offline importer refuses incorrect archive bytes before extraction (0.371605ms)
|
||||
✔ heap measurement runs explicitly in an isolated GC-enabled process (0.145852ms)
|
||||
✔ verified pinned ZIP round-trips to all original text files (390.61197ms)
|
||||
ℹ tests 8
|
||||
ℹ suites 0
|
||||
ℹ pass 8
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3637.750779
|
||||
@@ -0,0 +1,458 @@
|
||||
✔ row 194 T16 Settings Agents: unavailable read-only, launch follows its request, confirms, reason and refusal (13120.49989ms)
|
||||
✔ row 256 T7 T8 the human page offers Delete on a creative instance, the tester page never does (594.900276ms)
|
||||
✔ row 194 T16: each agent action makes one call with only its keys, logs the user and answers the request (19.279785ms)
|
||||
✔ row 194 T16: the boundary refuses before any bus call (8.864838ms)
|
||||
✔ row 194 T16: read-only when send is off or no writer is wired; no route has a --send exception (4.555721ms)
|
||||
✔ row 194 T16: a bus refusal names its reason and nothing changed; an unknown outcome or a bad shape is never retried (3.135758ms)
|
||||
✔ row 194 T16: the request read takes one id, adds the refusal reason and refuses an unexpected shape (4.707014ms)
|
||||
✔ row 194 T16: delete joins add and change on /api/instances with only revision, instance and op (2.172666ms)
|
||||
✔ row 194 T16 D4.1: the tester listener launches, terminates and reads requests; regenerate, veto and delete are refused with no call (4.058214ms)
|
||||
✔ row 194 N5: agents.js is served as JavaScript and index.html loads it once (1.320432ms)
|
||||
✔ row 239 T6: a delete closed -revoked says the tokens may already be revoked, and outcome-unknown-revoked is an open outcome (2.197169ms)
|
||||
✔ Q169 the signed-in human names a seat and stops the assistant, one bus call each, logged with the user (15.385947ms)
|
||||
✔ Q169 the name and stop routes need send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (7.953505ms)
|
||||
✔ Q169 the name and stop routes take JSON within 4 KiB and only their own keys, all text (4.763975ms)
|
||||
✔ Row 167 a null body on the name and stop routes is 400 invalid-request with no write, and the next request is answered (2.613751ms)
|
||||
✔ Q169 a bus refusal says nothing changed; a lost or odd answer says the change may have been made (4.454523ms)
|
||||
✔ Q169 the names read is a bus read like the others (1.187961ms)
|
||||
✔ the Board: held roles with no machine sessions, a claim with no address, the toggle kept across a reload, and Message addressed to the role (2826.381092ms)
|
||||
✔ a Seen click while a Board read is in flight waits for that read, then posts (1097.524589ms)
|
||||
✔ a redirect answer on a Board read, a bus read, a conversation read, a board POST, a module read and a send signs the page out: one line, nothing stale, no further reads (4974.029468ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (1167.906408ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (1361.244374ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (0.895092ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (2997.413698ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (71.009621ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (35.642956ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (12.415355ms)
|
||||
✔ replies route serves the replies to one message from the reader view (33.836567ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (53.12275ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (25.300216ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (1652.166732ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (179.503984ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (528.73483ms)
|
||||
✔ serve refuses a --business value that is not a business id (288.679832ms)
|
||||
✔ WebUI channel off returns 503 and named recovery for every chat route without a chat operation (32.840935ms)
|
||||
✔ WebUI only selection keeps web chat enabled while retaining the old read envelope (2.07606ms)
|
||||
✔ WebUI channel-disabled recovery text is fixed and never echoes transport text (1.29965ms)
|
||||
✔ chat browser: list/create named projects and threads, hostile labels inert and no implicit writes (641.425225ms)
|
||||
✔ chat browser: thread drafts, receipts and cursor history stay isolated; refresh never writes (734.207819ms)
|
||||
✔ chat browser: uncertain writes retain draft, ownership refusal clears it, read-only cannot send (733.502029ms)
|
||||
✔ chat browser: a late read from A cannot paint B; sign-out clears private state and stops reads (956.0031ms)
|
||||
✔ chat browser: page forward and deduplicate replies; pending and byte-limit submissions do not duplicate (653.519467ms)
|
||||
✔ chat browser: scrolled history stays put, New messages and an own send return to the tail (651.214034ms)
|
||||
✔ chat browser: automatic polling follows a reply without a POST or a lost draft (10567.99751ms)
|
||||
✔ chat browser: display stays bounded while every forward page remains reachable (646.919125ms)
|
||||
✔ chat browser: a confirmed visible message replaces its pending receipt; malformed receipts stay unknown (666.089089ms)
|
||||
✔ chat browser: a refusal elsewhere forgets private chat drafts (649.343576ms)
|
||||
✔ chat browser: a late project creation receipt cannot navigate away from another thread (734.318814ms)
|
||||
✔ C2-09a picker: no silent choice, duplicate hostile labels stay inert, selection survives refresh and reaches POST (633.758399ms)
|
||||
✔ C2-09a picker: metadata failure, empty set and removed pick explain disabled state and recover without switching (705.839055ms)
|
||||
✔ C2-09a picker: a single instance is chosen explicitly but read-only mode cannot create a thread (603.87131ms)
|
||||
✔ 264e chooser: two sessions keep drafts, cursors, deep links and counts independent (709.28204ms)
|
||||
✔ 264e clear confirmation and status never resend, force-stop or change selection (646.479049ms)
|
||||
✔ 264e new session keeps its operation key after an unknown response and uses no implicit open (629.483981ms)
|
||||
✔ 264e cancel shows before/after retirement and never starts or replays work (748.8715ms)
|
||||
✔ 272 B1: a bad-session deep link preserves the draft and lost clear operation on A (679.330701ms)
|
||||
✔ C2-09a metadata uses the human listener bus, projects labels only and refuses unauthenticated reads (17.636401ms)
|
||||
✔ C2-09a metadata uses the private tester listener own bus and no human transport (2.812051ms)
|
||||
✔ C2-09a metadata refuses missing or malformed reads and never echoes untrusted fields (3.36452ms)
|
||||
✔ C2-09a metadata stays behind the SSO session check and a GET never refreshes session activity (2.031401ms)
|
||||
✔ 272 fallback: granted legacy route keeps default history, send and open without session controls (551.938108ms)
|
||||
✔ 272 fallback guard: granted explicit-session route refuses without default history or writes (446.9789ms)
|
||||
✔ 272 fallback guard: human-required catalog failure never enables default-thread access (438.616041ms)
|
||||
✔ 272 fallback guard: timeout catalog failure never enables default-thread access (332.141995ms)
|
||||
✔ 272 fallback guard: no-bus-host catalog failure never enables default-thread access (345.68892ms)
|
||||
✔ 272 fallback guard: malformed catalog failure never enables default-thread access (457.527881ms)
|
||||
✔ 272 fallback guard: owner route retains chooser and explicit default-session addressing (356.922913ms)
|
||||
✔ chat reads map only fixed verbs, strict forward paging and pure GET (22.513107ms)
|
||||
✔ chat refuses malformed and repeated query/id values before transport (5.490672ms)
|
||||
✔ chat project.create: one verified write, fixed identity/class and private log (2.177988ms)
|
||||
✔ chat project.create: each existing Console write fence makes zero bus calls (6.741435ms)
|
||||
✔ chat project.create: refusal or lost/malformed receipt never retries or invents success (3.881328ms)
|
||||
✔ chat thread.create: one verified write, fixed identity/class and private log (1.282899ms)
|
||||
✔ chat thread.create: each existing Console write fence makes zero bus calls (6.492244ms)
|
||||
✔ chat thread.create: refusal or lost/malformed receipt never retries or invents success (3.376929ms)
|
||||
✔ chat thread.send: one verified write, fixed identity/class and private log (1.218434ms)
|
||||
✔ chat thread.send: each existing Console write fence makes zero bus calls (5.297186ms)
|
||||
✔ chat thread.send: refusal or lost/malformed receipt never retries or invents success (3.045893ms)
|
||||
✔ chat thread.open: one verified write, fixed identity/class and private log (1.093096ms)
|
||||
✔ chat thread.open: each existing Console write fence makes zero bus calls (5.706024ms)
|
||||
✔ chat thread.open: refusal or lost/malformed receipt never retries or invents success (3.010259ms)
|
||||
✔ chat createThread receipt requires an instance even when its id is valid (1.918607ms)
|
||||
✔ chat write receipts expose only the fields belonging to their operation (3.206521ms)
|
||||
✔ chat validates names, body, project and strict empty open payload (4.821072ms)
|
||||
✔ chat read failures redact ownership and refuse invalid transport arrays (5.618177ms)
|
||||
✔ REQ-USER-2 real broker: owned catalog, create/name/filter, isolated history and 403/404 without mutations (90.748147ms)
|
||||
✔ chat repeats every POST boundary even when called behind an admitted request (0.833696ms)
|
||||
✔ configured Console process injects chat through its existing human transport (133.521935ms)
|
||||
✔ chat assets, GET purity, method and unknown-route refusals are explicit (11.226856ms)
|
||||
✔ the chat seam names the transport it is given: the human one and the tester one both pass, anything else is a TypeError (0.427616ms)
|
||||
✔ C2-09a createThread forwards an explicit instance through the existing channel (3.442901ms)
|
||||
✔ C2-09a malformed instance and extra thread fields refuse before the channel write (3.151853ms)
|
||||
✔ 264e session catalog/create and explicit selectors keep the human transport and strict keys (4.390959ms)
|
||||
✔ 264e session writes retain Origin/identity fences and explicit selector errors (2.283871ms)
|
||||
✔ 264e clear envelope is projected, pending is preserved and GET never repeats the clear (3.67385ms)
|
||||
✔ 264e cancel is an authenticated, confirmed human write and retains retirement distinction (6.231344ms)
|
||||
✔ 272 N1: malformed clear/cancel write receipts get one redacted outcome-unknown audit line (4.493454ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (1005.423417ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1370.56904ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (954.423136ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (661.083302ms)
|
||||
✔ J16: no controls without sso and decide, read-only reasons, terminal lines kept, and only an eligible grant gets the panel (4815.646894ms)
|
||||
✔ J17: one POST per click, confirm then answer, cancel, refusals and an unknown outcome never retry (3929.059474ms)
|
||||
✔ a sign-in redirect at either step signs the page out and answers nothing (1457.865765ms)
|
||||
✔ a signed-in owner confirms and answers, with the session's provider, issuer and sub as the subject (70.013805ms)
|
||||
✔ the listener gates: no session, a cross-origin page, a loopback Host, a GET, and a sign-out between the steps (92.743885ms)
|
||||
✔ J16: without a decision transport the routes are read-only and /api/config says no; startServer refuses decide without sso or send (172.195474ms)
|
||||
✔ J19: a granted user's session gets decide false and the user page config, and a decision POST stops at owner-only (37.306927ms)
|
||||
✔ N4: decision.resolve has no Console route, the --send caller names no decision verb, and consoleCall takes only the two console verbs (36.688496ms)
|
||||
✔ a confirmation then a resolve answers once with the held choice and digest, and a second resolve refuses (2.676004ms)
|
||||
✔ deny resolves without a grant field (0.334827ms)
|
||||
✔ the gates before the body: method, read-only, sign-in and same origin (0.377549ms)
|
||||
✔ a bad body, id or content type refuses before the bus (0.595313ms)
|
||||
✔ a choice that is neither key is decision-stale, and a malformed preview is confirmation-unavailable (0.51847ms)
|
||||
✔ bus refusals map by code, and an unknown or missing code is unavailable at preview and outcome-unknown at resolve (1.757134ms)
|
||||
✔ a confirmation binds the session, decision and choice, and any mismatch spends it (0.48989ms)
|
||||
✔ a session that ends between the steps refuses before the bus (0.311432ms)
|
||||
✔ confirmations expire after the TTL, cap at the limit without dropping a live one, and are one use (0.195193ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (580.425029ms)
|
||||
✔ the editor: the list, hostile markdown that runs nothing, the draft, and each save receipt (1538.990805ms)
|
||||
✔ the Console's CSP stands and the editor runs under it; hostile markdown on the load path runs and loads nothing (878.229666ms)
|
||||
✔ hostile content pasted into the editor runs and loads nothing, and a bad link target is refused in the link bar (684.461512ms)
|
||||
✔ a pasted image span is kept only for a target the link gate accepts, and a save writes only those (550.259133ms)
|
||||
✔ a switch of view reads nothing; a reload, and a trip away and back, each read the file exactly once (2485.105458ms)
|
||||
✔ an edit rewrites only the block touched; a save sends the rest byte for byte; the Source view carries the draft (985.988893ms)
|
||||
✔ a file the editor cannot write back byte for byte opens as Source, says so, cannot save, and the Visual button works once it can (1940.942348ms)
|
||||
✔ a link title pasted with line breaks in it is written on one line, so no HTML block comes of it (545.329956ms)
|
||||
✔ an image alt pasted with line breaks in it is written on one line, so no HTML block comes of it (541.469322ms)
|
||||
✔ an image title pasted with line breaks in it is written on one line, so no HTML block comes of it (541.761624ms)
|
||||
✔ a code block info string pasted with line breaks in it is written on one line, so no HTML block comes of it (529.938542ms)
|
||||
✔ a link title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (589.242396ms)
|
||||
✔ an image title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (496.075229ms)
|
||||
✔ a link title holding a trailing backslash is written with the backslash escaped, so the title stays closed (495.515484ms)
|
||||
✔ an image title holding a trailing backslash is written with the backslash escaped, so the title stays closed (559.520726ms)
|
||||
✔ a code block info string with a backtick, pasted, is dropped on Save, so the fence stays closed (522.449511ms)
|
||||
✔ a code block info string with a backtick, as a tilde fence can have, is kept as source, not rewritten without it (560.892119ms)
|
||||
✔ the imported source is the tagged upstream source plus the recorded local changes, and nothing else (11.805683ms)
|
||||
✔ every package names its origin and license, and NOTICE lists each (0.494144ms)
|
||||
✔ the build reads only imported source and the editor's own files; the bundle is the recorded build (5.659658ms)
|
||||
✔ MOSAIC_REQUIRE_EDITOR_BUILD: a missing or wrong esbuild skips without it and fails with it; a wrong binary always fails (3.429329ms)
|
||||
✔ the toolchain check, run as a program, exits non-zero without esbuild when the build is required (117.432817ms)
|
||||
▲ [WARNING] Ignoring this import because "third_party/editor/prosemirror-transform/src/mark.ts" was marked as having no side effects [ignored-bare-import]
|
||||
|
||||
third_party/editor/prosemirror-transform/src/index.ts:10:7:
|
||||
10 │ import "./mark"
|
||||
╵ ~~~~~~~~
|
||||
|
||||
"sideEffects" is false in the enclosing "package.json" file:
|
||||
|
||||
third_party/editor/prosemirror-transform/package.json:13:2:
|
||||
13 │ "sideEffects": false,
|
||||
╵ ~~~~~~~~~~~~~
|
||||
|
||||
✔ rebuilding from the source gives the same bytes (needs the pinned esbuild; MOSAIC_REQUIRE_EDITOR_BUILD=1 makes its absence a failure) (70.671292ms)
|
||||
✔ the editor routes serve the built files with their types and no cache (33.152872ms)
|
||||
✔ open and save with no edit writes the same bytes: the goals review and a doc with every element (14.952097ms)
|
||||
✔ open and save with no edit writes the same bytes: every repository doc under docs/ and spec/ (1354.689164ms)
|
||||
✔ the editor source builds markup only with DOM calls, and the bundle has no eval (2.049203ms)
|
||||
✔ the link gate and the image rule are the preview's (0.333066ms)
|
||||
✔ R6 G1 a granted user signs in, gets the user page and reaches the broker as itself (82.134951ms)
|
||||
▶ R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
✔ a listed user with no grant (40.944625ms)
|
||||
✔ a grant in another business (42.037987ms)
|
||||
✔ an expired grant (147.367931ms)
|
||||
✔ another user's grant (139.465978ms)
|
||||
✔ a sub in no file (63.607001ms)
|
||||
✔ a file the loader refuses (62.135909ms)
|
||||
✔ R6 G2 a subject with no live grant in this business is refused subject-unknown (496.381708ms)
|
||||
▶ R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
✔ grant removed (49.252373ms)
|
||||
✔ subject removed (62.077208ms)
|
||||
✔ grant expired (91.401492ms)
|
||||
✔ file now refused (43.84123ms)
|
||||
✔ R6 G3 a revoke counts on the next request: 401 and the session is gone (247.211075ms)
|
||||
✔ R6 G4 every owner route answers 403 owner-only before any bus call (91.916815ms)
|
||||
✔ R6 G5 the bus refuses every owner verb over the user transport on its own (76.065246ms)
|
||||
✔ R6 G9 a user chat sends only the plugins read over the owner transport (75.816462ms)
|
||||
✔ R6 G6 the user page holds the notice word for word (68.007509ms)
|
||||
✔ R6 G7 the hub label and plain peer links render from config on both pages (769.528743ms)
|
||||
✔ R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request (38.957533ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (51936.406505ms)
|
||||
✔ row 165 Settings: role instances read, add with a restart named, change without one, a refusal keeps the draft (991.579686ms)
|
||||
✔ row 165 Console: the signed-in human adds an instance through a real broker; validate exits 0, the log names the human and no restart is needed (row 194) (193.463891ms)
|
||||
✔ row 165 Console: the instance write needs send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (5.88349ms)
|
||||
✔ row 165 Console: the instance write takes JSON within 16 KiB and only its own keys; holder, credentials and an actor are refused (6.540542ms)
|
||||
✔ row 165 Console: a bus refusal says nothing changed; a lost or odd answer says the change may have been made (9.74122ms)
|
||||
✔ row 165 Console: the instances read takes no arguments, needs a bus and refuses an answer without a revision (4.463583ms)
|
||||
✔ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (130.905358ms)
|
||||
✔ row 254: each library sync code maps to its status (0.572723ms)
|
||||
✔ REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced and check failed; Reset posts one path; a save keeps the token limits (975.696605ms)
|
||||
✔ Row 167 with an assistant draft open, a token limit reset gives the assistant submit its revision (669.50689ms)
|
||||
✔ REQ-LIMIT-3 limits read returns only the token rows; no query or write is accepted (26.70046ms)
|
||||
✔ REQ-LIMIT-3 a limits answer without a token list is refused, not shown as no limits (7.994198ms)
|
||||
2026-10-24T05:53:21.357Z settings user="jason" changed
|
||||
✔ REQ-LIMIT-3 a Settings reset reaches the human write with its paths; a reset alone is enough (5.567108ms)
|
||||
✔ REQ-LIMIT-3 the Console reader asks the bus limits verb with no arguments (0.235819ms)
|
||||
✔ the preview source names none of the four string-to-markup sinks (0.846655ms)
|
||||
✔ headings, paragraphs, emphasis and inline code render (1.131421ms)
|
||||
✔ lists, block quotes and code blocks render (0.397914ms)
|
||||
✔ a link is made for http, https and relative targets, with rel and the target shown (0.889923ms)
|
||||
✔ a link with any other scheme, or an evasion of the scheme, stays source text (0.706367ms)
|
||||
✔ an image shows alt text and URL, is never an img element and never loads (0.140692ms)
|
||||
✔ raw HTML, entities and unsupported blocks show as source text, never as elements (0.276815ms)
|
||||
✔ markup inside code and inside link text stays text (0.153012ms)
|
||||
✔ hostile and pathological input stays within its look-ahead budget and under the nesting cap (31.828155ms)
|
||||
✔ trailing blanks and closing hashes are cut by a walk from the end, not a regex that retries inside a blank run (25.168256ms)
|
||||
✔ a link opener with no closer searches at most SPAN characters ahead (137.844248ms)
|
||||
✔ the look-ahead work of one render is a fixed multiple of the text, whatever the opener (712.427405ms)
|
||||
✔ past the budget an opener is source text, through the path all other text takes (14.790124ms)
|
||||
✔ a normal 200 KiB document renders exactly as it did without a budget, and spends little of it (18.081668ms)
|
||||
✔ a line separator inside a heading, a list item or a fence line does not end the marker line (0.486523ms)
|
||||
✔ a thematic-break line is a paragraph, every other marker line opens a list (0.367498ms)
|
||||
✔ every short line over space, tab, - * _ and a letter is a break exactly when the old regex said so (37.762407ms)
|
||||
✔ one line of marks does not throw, at 4 MiB, whatever the mark or the spacing (6004.208904ms)
|
||||
✔ CRLF and tabs are read like LF and spaces; empty input renders nothing (0.166234ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (84.34292ms)
|
||||
✔ Console Messages lists late unsolicited messages on polling with sender class time read state and trail; nav count includes beyond the page; reads never deliver (837.761934ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (1639.975112ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (1706.657673ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (2260.993696ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (1015.292402ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (1239.052354ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (1057.788945ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (1076.400999ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (1032.466352ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (1472.800878ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (273.230223ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (48.380515ms)
|
||||
✔ preview options are all three or none, and each is checked (6.428862ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (664.583698ms)
|
||||
✔ the CLI with all three flags serves the preview mode (146.290409ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (21.367837ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (4.268357ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (28.252132ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (13.326316ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (5.577205ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (18.130558ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (10.389963ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (5.672682ms)
|
||||
✔ processing browser: keyboard Stop, confirmed Resume, unknown outcome and persistent banner (765.31904ms)
|
||||
✔ processing browser: Q175 read-only interim keeps both writes disabled and names CLI recovery at 320px (589.583065ms)
|
||||
✔ processing browser: sign-out from config redirect blocks later refresh and writes (700.36576ms)
|
||||
✔ processing browser: sign-out from state redirect blocks later refresh and writes (692.089186ms)
|
||||
✔ processing browser: sign-out from write redirect blocks later refresh and writes (699.263448ms)
|
||||
✔ processing browser: sign-out from config sign-in required blocks later refresh and writes (680.376298ms)
|
||||
✔ processing browser: sign-out from state sign-in required blocks later refresh and writes (675.832789ms)
|
||||
✔ processing browser: sign-out from write sign-in required blocks later refresh and writes (657.336986ms)
|
||||
ROW191_LAYOUT {"width":400,"height":1000,"controls":97,"tree":324.0625,"main":1450.703125,"top":181.5,"bottom":217.5,"barBottom":129,"projectsTop":352.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":127,"tree":914,"main":914,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":420,"controls":127,"tree":334,"main":907.734375,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":800,"height":420,"controls":127,"tree":334,"main":1131.65625,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":618.59375,"order":"0","visible":true}
|
||||
✔ processing browser: static nav controls keep Stop visible, ordered and keyboard reachable (603.718005ms)
|
||||
✔ interim read-only controls show CLI recovery, disable both writes and retain a halted banner across views (7.334084ms)
|
||||
✔ Stop sends one empty halt and Resume confirms before sending the displayed generation (2.849813ms)
|
||||
✔ a cancelled resume never calls the writer (1.09297ms)
|
||||
✔ unknown writes are not retried or displayed as success and partial stops remain explicit (1.839818ms)
|
||||
✔ the persistent shell loads the control outside route-specific content with accessible status and native buttons (0.406741ms)
|
||||
✔ read-only Console can read processing state but neither human control writes without --send (18.503358ms)
|
||||
✔ Console halt and generation-bound resume use only the management writer and keep partial stops honest (3.909099ms)
|
||||
✔ processing write fences refuse cross-site, wrong origin, unnamed host, duplicate identity and bad payload before writer (6.015997ms)
|
||||
✔ tester cannot invoke either control writer and failures never echo backend text (3.943577ms)
|
||||
✔ processing writes use the existing SSO signer and still require --send and same-origin (4.161332ms)
|
||||
✔ processing handler independently refuses wrong origin and cross-site before its writer (0.550006ms)
|
||||
✔ a signed-in same-origin save reaches exactly one human operation and returns the new commit (17.503534ms)
|
||||
✔ a save needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (6.181398ms)
|
||||
✔ a save validates JSON, strict keys, text values and the 1 MiB body before the bus is asked (15.0983ms)
|
||||
✔ a body that is JSON but not an object is refused 400, not thrown on (4.888629ms)
|
||||
✔ the 1 MiB limit is this route only; Settings and messages keep 16 KiB (2.188725ms)
|
||||
✔ a bus refusal is reported as not saved with its own status; stale-base tells the user to reload (5.071686ms)
|
||||
✔ a save whose answer is lost or unusable is reported unknown and is never retried (3.102732ms)
|
||||
✔ the file list and one file read take strict queries and arrive in the expected shape (4.368356ms)
|
||||
✔ reads work without --send; an unusable bus answer is a bad response, a refusal keeps its status (2.429382ms)
|
||||
✔ /api/config says edit only where a save can pass (1.854521ms)
|
||||
✔ a repository-name project refusal is an HTTP conflict (0.512679ms)
|
||||
✔ proof startup refuses partial flags non-preview and invalid fixture file metadata or bytes (1169.034096ms)
|
||||
✔ serve argv carries only fixture proof path and header; startup explicitly reports proof on or off (243.582574ms)
|
||||
✔ CLI applies proof on the main listener and rejects that header on its tester listener (226.911465ms)
|
||||
✔ proxy proof admits named reads and sends, reads its fixture once, and never exposes the value (29.428285ms)
|
||||
✔ proxy proof refuses missing wrong short or repeated proof before auth and before any read (8.872495ms)
|
||||
✔ proxy proof header is refused on other Hosts and on the tester listener (4.215652ms)
|
||||
✔ proof options reject unsafe or colliding header names without disclosing file contents (0.628262ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (1.187629ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.131716ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (324.251778ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (166.918485ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (702.513606ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (8.976419ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (6.080389ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (6.341439ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (98.9594ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (16.845946ms)
|
||||
✔ bus-status: a refused boot shows reason, file and last good load; a live host or an ok boot clears it (6.721379ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (636.416956ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (20712.001803ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Settings: the routing table lists every instance, saves a harness, model, thinking and fallback to the file, and keeps the draft on each refusal (1289.24343ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Console: the signed-in human saves a route through a real broker; each refusal says nothing changed and leaves the file and the log (103.948331ms)
|
||||
✔ row 65 Console: the routes write needs send on, a writer, the public name, the user, the exact Origin and same-origin Fetch Metadata (6.771368ms)
|
||||
✔ row 65 Console: the routes write takes JSON within 16 KiB with a revision and routes only; an actor is refused and routes are never logged (7.008611ms)
|
||||
✔ row 65 Console: a bus refusal maps to its status and says nothing changed; a lost or odd answer says the change may have been made (25.506113ms)
|
||||
✔ row 65 Console: the routes read takes no arguments, needs a bus and refuses an odd answer (5.997376ms)
|
||||
✔ the send box: held roles, the draft and caret across a refresh, each receipt, one POST per Send, and the trail link; the board Reply on a 4xx and a 5xx (2339.8884ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (11096.659422ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (5.436794ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (449.118647ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (150.581681ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (7.231271ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (5.874475ms)
|
||||
✔ config says send only on the public name with --send on (2.230966ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (3.855927ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (12.123809ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (2.548018ms)
|
||||
✔ a crafted role name cannot forge a log line (1.815113ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (6.025781ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (82.134733ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (32.652181ms)
|
||||
✔ loopback host and board origin fail closed (6.088571ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (71.586234ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (39.352406ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (398.535455ms)
|
||||
✔ tester service render carries the tester port alongside existing flags (95.469287ms)
|
||||
✔ tester service refuses forwarded 7332 even with another main port (58.686381ms)
|
||||
✔ tester service invalid ports refuse render and install with exit 4 and leave the unit alone (703.380562ms)
|
||||
✔ tester service counts the digits as written before it strips leading zeros (65.126118ms)
|
||||
✔ tester service install and status show the tester port without changing send reporting (358.896996ms)
|
||||
✔ render prints the unit without the preview flags (33.100741ms)
|
||||
✔ render carries every serve flag, the preview set included (34.839285ms)
|
||||
✔ a partial preview set is refused (94.945574ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (1668.211167ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (105.101552ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (193.571828ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (44.720501ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (441.860555ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (265.638824ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (629.337843ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (173.097355ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (279.635691ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (667.172745ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (450.623337ms)
|
||||
✔ uninstall and status take no --send (149.404818ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (654.290201ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (210.540626ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (293.268822ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (631.243119ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (93.452901ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (1786.499417ms)
|
||||
✔ a preview value cannot start with - or . (190.355117ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (521.398589ms)
|
||||
✔ proxy proof render carries both flags but never opens the fixture file (35.027753ms)
|
||||
✔ proxy proof render refuses incomplete non-preview missing unsafe-mode and repo/copy paths (378.504771ms)
|
||||
✔ proxy proof status retains send and tester reporting with both proof flags (156.354779ms)
|
||||
✔ row196 socket flag renders only for supporting targets and refuses mixed transports (91.044454ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (1149.397483ms)
|
||||
✔ Q169 Assistant section: gated, changed fields only, name and stop posts, retained draft and no automatic retry (1202.434375ms)
|
||||
✔ Row 167 a refused names read gives the refused usage page; another names failure keeps the Assistant section (828.93179ms)
|
||||
✔ Row 167 with a limits draft open, an assistant save gives the limits submit its revision and the typed limit stays (698.454133ms)
|
||||
✔ Row 167 with an assistant draft open, a launch save gives the assistant submit its revision, and reload reads the current one (686.551532ms)
|
||||
2026-10-24T05:54:48.215Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (16.774402ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (5.785092ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (4.72348ms)
|
||||
✔ Q169 a Console change of the assistant reaches the human operation unchanged, and the log names the signed-in user (2.505956ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (0.744344ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (1.776009ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (3.276718ms)
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":212.5,"tree":914,"main":914}
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (5611.131411ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (2403.421938ms)
|
||||
✔ a 401 sign-in required on each fetch path signs the page out with a sign-in link and stops the refresh; another 401 stays an error (5576.793869ms)
|
||||
✔ the Sign out form shows only when /api/config says sso, and posts to /auth/logout (989.677029ms)
|
||||
✔ positive: an RS256 provider signs in and sends one message, and the session reads (91.893637ms)
|
||||
✔ positive: an ES256 provider signs in and sends (15.625044ms)
|
||||
✔ positive: two providers each sign in their own subjects, and one provider's subject is unknown to the other (66.787533ms)
|
||||
✔ 1: a forged x-authentik header without a cookie is 401 on the API and a sign-in redirect on a page (42.725108ms)
|
||||
✔ 2: an HS256 ID token keyed with the client secret and a real RSA kid is refused (23.753077ms)
|
||||
✔ 3: a wrong aud, and an aud list holding another client with no azp, are refused (98.135791ms)
|
||||
✔ 4: a token that expired 10 minutes ago is refused (36.300718ms)
|
||||
✔ 5: another issuer, and the issuer without its trailing slash, are refused (51.400149ms)
|
||||
✔ 6: a nonce mismatch and a missing nonce are refused (38.472602ms)
|
||||
✔ 7: state mismatch, no login cookie, a replay, an expired record and two login cookies are refused (89.360558ms)
|
||||
✔ 8: the token request carries the PKCE verifier, and the OP refuses a wrong one (89.511481ms)
|
||||
✔ 9: alg none, a changed payload under a real kid, and an unknown kid even after the refetch are refused (122.689814ms)
|
||||
✔ 10: a valid token for a subject not configured is 403 with one subject-unknown line (30.706987ms)
|
||||
✔ 11: start refusals: HS or none algs, reserved names, HS-only discovery, an empty JWKS, no S256, an issuer mismatch, a short RSA key (260.021698ms)
|
||||
✔ 11: slot files: a symlink, mode 0644, mode 0640 and a file under the repository are refused, and no message holds the value (2.066508ms)
|
||||
✔ 11: the CLI refuses --sso with the old flags and without console.sso, and starts with a good one (1041.934871ms)
|
||||
✔ 12: under --sso the tester listener refuses each kind of sign-in, has no /auth, and startServer refuses tester with sso (72.329262ms)
|
||||
✔ 13: idle and absolute expiry, logout then reuse, a cross-origin logout, and background reads that don't keep a session (61.151874ms)
|
||||
✔ 13: the CSP lets the Sign out form reach the provider's end-session origin, and only under --sso (83.673402ms)
|
||||
✔ 14: open redirects land on /, and a path on the Console lands there (43.000196ms)
|
||||
✔ 15: a callback for A carrying B's code and iss, and a provider= query, is refused (179.356624ms)
|
||||
✔ 16: a provider with RS256 and ES256 takes either, and refuses PS256 with a usable PS256 key in the JWKS (73.208932ms)
|
||||
✔ 17: 33 started logins leave 32 records, and the first one's callback finds none (83.204408ms)
|
||||
✔ 18: after a key rotation a sign-in works through one refetch, and the removed kid is refused (68.667369ms)
|
||||
✔ 20 and 21: the store holds no raw session id, and the cookie strings are exact (52.309181ms)
|
||||
✔ 22: a loopback Host is 403 on a page, a read and a write; /healthz on loopback is 200; a redirect_uri never comes from the Host (47.799698ms)
|
||||
✔ 23: a provider down at start leaves the Console up and 503, and the retry makes sign-in work (220.406623ms)
|
||||
✔ 19: no captured log line holds a token, code, state, nonce, verifier, client secret or session id (4.620525ms)
|
||||
✔ tester Console banner exposes only instance route and name editors and saves a route as tester (775.631416ms)
|
||||
✔ row 177: the tester Console chat composer is enabled and a send shows in its own thread (615.88403ms)
|
||||
✔ tester-port validates range tunnel main port and requires configured bus before listening (886.511066ms)
|
||||
✔ CLI tester listener binds a private Unix socket and spawns TESTER_CLI for every Console bus read (2552.468157ms)
|
||||
✔ main listener refuses x-mosaic-tester before any board or bus read for every Host (18.054152ms)
|
||||
✔ tester listener rejects preview binding and requires its fixed logical Host and no proxy auth header (101.487921ms)
|
||||
✔ tester listener writes only instances routes and names with tester receipts and actor (118.690594ms)
|
||||
✔ tester listener refuses every human-only write with no bus call (78.483618ms)
|
||||
✔ tester-off and tester-required reads return HTTP 403 (81.141155ms)
|
||||
✔ tester credentials routes and committed project reads keep secrets and paths bounded (151.187355ms)
|
||||
✔ row 177 W1: the tester chat makes, sends to, opens and reads its own thread as tester (22.229762ms)
|
||||
✔ row 177 W2: tester chat writes refuse a missing or foreign Origin, cross-site, text/plain and the proxy header with no bus call (11.497956ms)
|
||||
✔ row 177 W3: the human thread through the tester chat is thread-not-owned, and bus.tester off answers tester-off (27.903135ms)
|
||||
✔ row 177 W4: the main listener refuses x-mosaic-tester on chat reads and writes before the chat (1.945782ms)
|
||||
✔ Row 252: the tester listener has no decision answer routes, and its config never offers them (80.149998ms)
|
||||
✔ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (61.944248ms)
|
||||
✔ private tester refuses symlink, unsafe parent and long path without repairs (0.861963ms)
|
||||
✔ tester cleanup must leave a replacement inode untouched (1.465444ms)
|
||||
✔ pinned source keeps all original files and Princeton license bytes (36.50702ms)
|
||||
✔ two complete conversions match shipped bytes and pinned counts (4310.208292ms)
|
||||
✔ changed source, missing license, changed provenance and extra files refuse before output (64.379074ms)
|
||||
✔ data parser refuses malformed words, offsets, pointers, verb frames and trailing fields (0.3536ms)
|
||||
✔ index parser refuses wrong counts, nonexistent senses and incorrect lemma mapping (0.261971ms)
|
||||
✔ offline importer refuses incorrect archive bytes before extraction (0.385742ms)
|
||||
✔ verified pinned ZIP round-trips to all original text files (456.953703ms)
|
||||
✔ row278 browser lists held claims and release preserves role rows and totals (624.706447ms)
|
||||
✔ row278 browser claims 403 stays local and role section remains byte-equal (631.071998ms)
|
||||
✔ row278 browser renders only four escaped claim fields and never instance or other extras (576.777382ms)
|
||||
✔ row278 browser sign-out removes claims and prevents reuse of cached answers (640.568833ms)
|
||||
✔ row278 tester browser keeps role rows without requesting the owner claims section (467.05576ms)
|
||||
✔ row278 browser discards a late claims answer after sign-out (614.541457ms)
|
||||
✔ row278 HTTP read returns held claims without send and refuses arguments before the bus (29.289475ms)
|
||||
✔ row278 HTTP read preserves bus refusal and rejects a malformed result (8.116407ms)
|
||||
✔ row278 tester socket refuses thread claims explicitly with zero bus calls (4.484986ms)
|
||||
✔ row278 Console adapter calls argument-free thread.claims through the normal transport (124.206412ms)
|
||||
✔ 193d HTTP shares use the caller, plain/shared lists, exact args and write fences (75.202228ms)
|
||||
✔ 193d config supplies only the signed-in user live collaborator scopes and updates on downgrade (34.238825ms)
|
||||
✔ 193d viewer UI has no composer, resume, share, session mutations or create controls (516.480948ms)
|
||||
✔ 193d mixed grants keep viewer project read-only and collaborator own thread writable; downgrade hides controls (634.31794ms)
|
||||
✔ 193d owner has share controls on own threads and shared threads remain read-only (407.707207ms)
|
||||
✔ 193d bypassed UI checks still reach a refusing bus for every viewer write (481.425536ms)
|
||||
✔ 193d collaborator shares stay read-only and never expose reshare or wake controls (476.049259ms)
|
||||
✔ REQ-LIMIT-4 Settings shows all token kinds and unknown turns beside every total, as a read-only separate section (568.273975ms)
|
||||
✔ REQ-WEB-1 usage read keeps unknown counts and each token kind; no query or write is accepted (28.001462ms)
|
||||
✔ REQ-WEB-1 malformed usage is refused instead of shown as zero (5.810443ms)
|
||||
✔ R6-N1 with the userRoute gate open, a user session reaches no owner option over HTTP (121.924867ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (3101.488368ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1089.111187ms)
|
||||
✔ the server log names no config path when the system config refuses (317.48495ms)
|
||||
ℹ tests 425
|
||||
ℹ suites 0
|
||||
ℹ pass 425
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 277018.268665
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,134 @@
|
||||
# Row 265 / #1770 — project rooms
|
||||
|
||||
Current result: verified under sage-3942; slot returned in goldie-0034. Full bus 957/957, sandbox 10/10, parser 15/15, rooms 18/18, twelve mutants killed, restored files green. Review/push packet below includes mos-1567 closure scope.
|
||||
|
||||
Goldie, 2026-10-24. Branch goldie/row265, base ec86f2c847f6154a0677c6ed23c87dabc2c665d0. Brief docs/plans/2026-10-20_case2-rows.md § C2-09b rooms at blob3a5ed91a4fb3462df67e6939ae6f18290ac9b453; design agents/gosalyn/work/case2/DESIGN.md §6. Sage-3915 transfers ownership from Rocko to Goldie and review from Dewey to Boyd. No old test edit authorized. All tests/mutants wait for Sage CLEAR; queued behind Poe, Bentina and Sage's326 gate.
|
||||
|
||||
## Design and clarified readings
|
||||
|
||||
A room is a bus project. Members are all loaded business instances whose definitions are in THREAD_DEFINITIONS, sorted by key. Canonical room threads have those keys as names and belong to the requesting human. open checks all name/instance/owner collisions before creating anything; it delegates missing inserts to existing thread.create dispatch, including default session metadata. send/read require the complete canonical set. No schema or migration change.
|
||||
|
||||
Sage-3921 settles two brief readings: the single captured timestamp applies to the room.send MESSAGE rows; existing route/delivery/event helpers keep their own timestamps in the same dispatch transaction. They are unchanged. A final EOF-terminated @name is a tag, so bare @continuity is invalid-request rather than a broadcast. Leading whitespace is accepted. Pure parseAddress returns {tags,body}, where body is the unaddressed remainder; room.send stores the ORIGINAL input unchanged. Export it from the bus index and expose a separate reusable test table for C2-10c parity.
|
||||
|
||||
send resolves tags against current #named results and member instance keys, deduplicates recipients, and refuses multiple member matches. It checks each target's current default-session fence and messaging rule, then inserts one normal INFO message, route and human.input per target under the existing request transaction. All message rows share one at. No tag broadcasts; tags after the first non-tag token remain plain text.
|
||||
|
||||
read includes all sessions of the caller's canonical threads, globally ordered by messages.seq. Each entry retains first-message fields plus thread/session/instance and adds ids (every grouped message id) and to (each instance once). Only consecutive human rows with identical at/body/class group, and a repeated instance starts a new entry, so same-millisecond identical sends remain separate. Paging limit counts merged entries (THREAD_PAGE defaults/max); clients continue after the last ids element. Cursor must belong to a room thread. Bounded raw scan limit*memberCount+1 completes requested groups without reading unbounded history. Sage-3921 accepts these routine choices as proposed in goldie-0030.
|
||||
|
||||
## Scope / steps
|
||||
|
||||
1. Read current thread.create/send/messages, transaction wrapper, naming and messaging helpers; audit source readers/pins. Claim265 via queue after next. Done/read, claim recorded at rev 3814 with op goldie-0265-start-1 (same-op replay confirmed).
|
||||
2. Add parser + index export, only three room cases in broker, README verb rows and new bus tests/fixtures. Do not modify existing thread/messaging/ACL/helper behavior.
|
||||
3. New tests cover all brief cases, atomicity across message/route/input rows, ownership/projects, permissions, paging/grouping and session fences. No suites or test runs before CLEAR.
|
||||
4. Record exact refusal→test→mutant matrix here before requesting the slot; one targeted killed mutant per named refusal, restoration byte-checks. New files whole then bus suite under CLEAR, serial timeout-k10, fresh31-byte mktemp TMPDIR plus UTC/load/busy header.
|
||||
5. Before in-review, grep packages/*/tests for broker.mjs/TESTER_VERBS and changed-file pins. Push own branch and hand tip/base/manifest directly to Sage/Boyd. Sage owns request posting/landing.
|
||||
|
||||
No Console/Discord/CLI room surface or cross-agent reply visibility added. No tester verb, role, schema or secret change. No host action or credential access.
|
||||
|
||||
## New tests and refusal / mutant matrix
|
||||
|
||||
Only new test files and a new reusable vector fixture are added. No pre-existing test changes. Parser table: address.test.mjs consumes address-cases.mjs, including final EOF tags, leading whitespace, tags after the first word, repetition and blank/tag-only remainder. Broker cases: rooms.test.mjs covers sorted/reused canonical threads, other-instance and other-owner collisions, all three verbs' human/tester gates, missing projects/member set, partial/foreign/wrong-instance rooms, key and current-name addressing, ambiguity/same-member name+key, unknown/non-member/old-name tags, tags-only bodies, one/many/broadcast targets, original bytes, shared message timestamp, routes/deliveries/human.input, denied last recipient, clearing last session, all-session merged reads, agent reply metadata, merged paging/default/max, foreign/noncanonical/absent cursors, and same-at repeated sends. Synthetic historical rows append valid rows; they do not rewrite evidence or clocks. The same-at batches repeat the first recipient at each boundary, proving identical sends remain distinct.
|
||||
|
||||
Each named refusal has one mutation of its enforcement, run against the entire new rooms.test.mjs file. The driver mutants.py stores the exact original→mutant diff in logs/Mxx-*.diff, logs UTC/load/busy/TMPDIR, accepts only Node exit 1 with the named target test failing, and restores/verifies full broker bytes and SHA256 in finally. No mutation is retained. Generic pre-existing request/schema/argument policies are exercised through existing code; the matrix names the refusals added or deliberately inherited by these room cases.
|
||||
|
||||
| Mutant | Refusal | Guard relaxed | Required failing new test |
|
||||
| --- | --- | --- | --- |
|
||||
| M01-human | human-required | remove common room human check | room verbs require the human |
|
||||
| M02-tester | tester-refused | add the three room verbs to TESTER_VERBS | room verbs require the human (explicit absence pin) |
|
||||
| M03-project | project-not-found | remove room project lookup check | room verbs refuse missing projects |
|
||||
| M04-members | thread-instance-missing | allow empty member set | room verbs refuse missing projects |
|
||||
| M05-collision | room-thread-name-taken | remove canonical owner/instance collision check | room.open preflights instance collisions |
|
||||
| M06-open | room-not-open | remove missing canonical thread check | send/read refuse a missing canonical member |
|
||||
| M07-body | invalid-request | allow empty parsed remainder | unknown or non-member addressees and tags-only bodies |
|
||||
| M08-unknown | unknown-addressee | allow zero member matches | unknown or non-member addressees and tags-only bodies |
|
||||
| M09-ambiguous | addressee-ambiguous | allow multiple member matches | a legacy name colliding with another member key |
|
||||
| M10-messaging | message-sender-refused | omit room target messaging check | messaging denial on the last target |
|
||||
| M11-cursor | message-not-found | unknown room cursor falls back to seq zero | room.read groups one send, pages |
|
||||
| M12-fence | session-clearing | omit room target session fence | a clearing last member fences the whole send |
|
||||
|
||||
For M02, the existing human check remains a second barrier: the mutant deliberately relaxes only the tester allowlist; the new explicit absence assertion kills it without loosening or running the old pin. Existing helpers keep their full session-state behavior. No helper or schema mutation ships.
|
||||
|
||||
## Reader / hash audit
|
||||
|
||||
`rg -n 'broker\.mjs|TESTER_VERBS' packages/*/tests --glob '!fixtures/*'` produced reader-grep.txt (133 hits at audit time). These are imports except the concrete source read at bus/tests/limit-checks.test.mjs:323-326 (forbidden limit-check/token-limit imports) and the tester set assertions at bus/tests/tester-writes.test.mjs:18-32. The latter stays byte-for-byte unchanged. bus/tests/viewer.test.mjs:116 builds its coverage set from exported verb lists; rooms do not widen those lists. No old test needs an edit.
|
||||
|
||||
Pin check: recursively inspect packages/*/tests plus scripts/test-*.sh for changed-file mentions (broker.mjs, address.mjs, packages/bus/README.md, packages/bus/src/index.mjs) with literal 64-hex values within eight lines: pin-context-grep.txt records no hits. A direct same-line hash/digest/sha256 grep finds registrySha256 imports only, which hash host registry data rather than broker bytes. The independent channel-proof.test.mjs baselines cover proof/transport files, none changed here. Positive control: pin-positive-control.txt records actual schema.sql / SCHEMA_SHA256 / createHash references in bus/tests/schema.test.mjs and bus/src/store.mjs. Schema bytes are unchanged.
|
||||
|
||||
## Requested serialized slot plan (not run)
|
||||
|
||||
At each run: fresh `TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)`, date -u, uptime, and `bash /mnt/storage/scratch/sage-busy-now.sh 2>/dev/null | tail -1` in the log. No concurrent test runs. Ignored root node_modules symlink targets the canonical checkout; it will not be committed.
|
||||
|
||||
1. `timeout -k 10 120 node --test packages/bus/tests/address.test.mjs` whole.
|
||||
2. `timeout -k 10 120 node --test packages/bus/tests/rooms.test.mjs` whole.
|
||||
3. `timeout -k 10 600 node --test --test-concurrency=1 packages/bus/tests/*.test.mjs` bus suite whole.
|
||||
4. `python3 agents/goldie/work/queue-265/mutants.py`: M01 through M12 serially, each runs `timeout -k 10 120 node --test packages/bus/tests/rooms.test.mjs` whole, fresh TMPDIR/header; stop on a live mutant or wrong failure. Record each diff and restoration digest.
|
||||
5. Restored address.test.mjs whole and rooms.test.mjs whole separately, same commands as 1/2 and fresh directories/header each.
|
||||
|
||||
Estimate 5–10 minutes; bus timeout ceiling 10 minutes, individual new/mutant ceilings 2 minutes each. On first new-test red, record/report it. On any old-test red or need for old-test change, stop and ask Sage. Static node --check for new modules/tests and git diff --check passed; no tests or mutants run yet.
|
||||
|
||||
## First slot STOP — sage-3932
|
||||
|
||||
2026-10-24T04:27Z: stopped on an old-test red. No old test changed or rerun, no mutant executed, no product adjustment after the failure, no commit/push claiming verification.
|
||||
|
||||
| Step | Exit | Counts / disposition | Log |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 address whole | 0 | 14 pass, 0 fail/skip/cancel; 58.559 ms | logs/01-address.log |
|
||||
| 2 rooms whole | 0 | 18 pass, 0 fail/skip/cancel; 1067.524 ms | logs/02-rooms.log |
|
||||
| 3 bus whole | 1 (stopped) | partial 619 top-level ok, 1 not ok; not a completed-suite result | logs/03-bus.log |
|
||||
| 4 twelve mutants | not run | stop rule | none |
|
||||
| 5 restored files | not run | stop rule; no mutations were applied | none |
|
||||
|
||||
All three run headers use UTC/date, uptime, helper CLEAR and distinct 31-byte scratch TMPDIRs. Bus began04:25:22Z at load2.80/2.91/2.61, TMPDIR /mnt/storage/scratch/tmp/LaZAaK. Parser used /mnt/storage/scratch/tmp/rqcTH7; rooms has its own header. Explicit TAP reporter was selected for rooms/bus and the mutant driver so failure detection/counts are stable across Node26's default spec reporter; this changes reporting only.
|
||||
|
||||
The old red is project-runtime.test.mjs:12, assertion:14, test620, “Broker keeps the sandbox client import closure: only the listed imports, no lazy project import”. Actual imports include the new `import { parseAddress } from './address.mjs';`; the pinned array omits it. ERR_ASSERTION deepStrictEqual; individual duration2.469251ms. The full expected/actual/stack is in 03-bus.log. No other red was observed before stopping.
|
||||
|
||||
Reader-audit correction: reader-grep.txt:100 already contains project-runtime.test.mjs:13, a direct broker source read. The earlier prose incorrectly described the import-closure assertion as a normal import; I missed this old source pin during inspection. This is not a hash pin. Follow-up static trace finds seat/src/sandbox.mjs:50-60 RUNNER_FILES includes broker and landing but not address; seat/tests/sandbox.test.mjs:45-53 compares this mount list to the recursively imported source closure. A parser dependency therefore also needs the one new RUNNER_FILES path. Both an old-test pin change and this out-of-owned-files product change require Sage/Mos approval before edits. Recommended bounded change: add only the parser import to the project-runtime expected imports, add only packages/bus/src/address.mjs beside broker in RUNNER_FILES, and leave the generic sandbox closure test unchanged.
|
||||
|
||||
Broker bytes remain SHA256 71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781. git diff --check still passes. Slot returned in goldie-0032, pending ruling/new CLEAR; no reruns.
|
||||
|
||||
## mos-1567 approved closure correction / second slot request
|
||||
|
||||
Sage-3940 relays Mos's mos-1567 and widened row scope (sage-3939). Applied exactly the approved pre-existing-file edits: project-runtime.test.mjs adds two lines at17-18, one Row265 comment and the parser import in the pinned array, no other line changed (numstat2 added/0 deleted); seat/src/sandbox.mjs adds only `packages/bus/src/address.mjs` immediately after broker in RUNNER_FILES. seat/tests/sandbox.test.mjs stays byte-for-byte unchanged. The parser remains a separate pure module. A NEW block in address.test.mjs asserts `doesNotMatch(/\bimport\b/)` on its source, with a node:fs import in that new test file; no existing assertion is moved or weakened.
|
||||
|
||||
Review packet must explicitly call out the RUNNER_FILES mount-list entry for Boyd's review. This is a read-only source-file mount needed by the broker's static dependency, with no new module dependency or role/verb authority. The first-run import pin failure remains recorded above and in logs/03-bus.log; new logs will have rerun-prefixed names, preserving first-slot bytes.
|
||||
|
||||
Requested rerun order, awaiting CLEAR after Bentina: first `timeout -k 10 180 node --test --test-reporter=tap packages/seat/tests/sandbox.test.mjs` whole, then the original five steps again from step1: address whole (now15 cases), rooms whole, bus whole with concurrency1 (includes project-runtime whole), M01–M12 whole-room-file mutants, then restored address and rooms files separately. Explicit TAP reporter is used throughout for stable logs/kill matching; timeouts otherwise120 per new/mutant run and600 for bus. Each run still gets fresh short mktemp TMPDIR, date/uptime/busy header, serialized with the same stop rule. Estimate5–10minutes. Only static node --check and git diff --check run since the STOP; all pass. No new test run yet.
|
||||
|
||||
## Verified result — sage-3942 CLEAR, slot returned goldie-0034
|
||||
|
||||
All rerun positives exit 0 with zero failures, skips or cancellations. Full bus suite passed 957/957, including the approved project-runtime import pin. The unchanged sandbox test passed 10/10, including its recursive mount/import-closure assertion. New parser cases passed 15/15, rooms 18/18, and both passed again after restoration. No tests run after slot return.
|
||||
|
||||
| Run | Exit | Pass / fail | Duration ms | UTC / load / helper | Log |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| rerun-00-sandbox | 0 | 10 / 0 | 5526.027699 | Sat Oct 24 04:31:35 UTC 2026; 23:31:35 up 10 days, 14:51, 2 users, load average: 2.83, 2.91, 2.73; CLEAR | [log](logs/rerun-00-sandbox.log.gz) |
|
||||
| rerun-01-address | 0 | 15 / 0 | 54.634817 | Sat Oct 24 04:31:45 UTC 2026; 23:31:45 up 10 days, 14:51, 2 users, load average: 2.54, 2.84, 2.71; CLEAR | [log](logs/rerun-01-address.log.gz) |
|
||||
| rerun-02-rooms | 0 | 18 / 0 | 921.788987 | Sat Oct 24 04:31:52 UTC 2026; 23:31:52 up 10 days, 14:51, 2 users, load average: 2.42, 2.81, 2.70; CLEAR | [log](logs/rerun-02-rooms.log.gz) |
|
||||
| rerun-03-bus | 0 | 957 / 0 | 118397.418425 | Sat Oct 24 04:32:01 UTC 2026; 23:32:01 up 10 days, 14:51, 2 users, load average: 2.28, 2.77, 2.68; CLEAR | [log](logs/rerun-03-bus.log.gz) |
|
||||
| rerun-05-restored-address | 0 | 15 / 0 | 54.587943 | Sat Oct 24 04:35:02 UTC 2026; 23:35:02 up 10 days, 14:54, 2 users, load average: 3.10, 3.07, 2.83; CLEAR | [log](logs/rerun-05-restored-address.log.gz) |
|
||||
| rerun-05-restored-rooms | 0 | 18 / 0 | 991.877991 | Sat Oct 24 04:35:11 UTC 2026; 23:35:11 up 10 days, 14:55, 2 users, load average: 3.17, 3.09, 2.84; CLEAR | [log](logs/rerun-05-restored-rooms.log.gz) |
|
||||
|
||||
Mutant driver exit 0; all twelve target tests killed their guard relaxation with Node exit 1. Every mutant log records exact bytes restored to broker SHA256 `71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781`. Driver receipt: [rerun-04-mutants-driver.log](logs/rerun-04-mutants-driver.log.gz).
|
||||
|
||||
| Mutant | Node exit | Pass / fail | Exact diff | Raw log |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| M01-human | 1, killed | 16 / 2 | [diff](logs/M01-human.diff) | [log](logs/M01-human.log.gz) |
|
||||
| M02-tester | 1, killed | 17 / 1 | [diff](logs/M02-tester.diff) | [log](logs/M02-tester.log.gz) |
|
||||
| M03-project | 1, killed | 17 / 1 | [diff](logs/M03-project.diff) | [log](logs/M03-project.log.gz) |
|
||||
| M04-members | 1, killed | 17 / 1 | [diff](logs/M04-members.diff) | [log](logs/M04-members.log.gz) |
|
||||
| M05-collision | 1, killed | 14 / 4 | [diff](logs/M05-collision.diff) | [log](logs/M05-collision.log.gz) |
|
||||
| M06-open | 1, killed | 17 / 1 | [diff](logs/M06-open.diff) | [log](logs/M06-open.log.gz) |
|
||||
| M07-body | 1, killed | 17 / 1 | [diff](logs/M07-body.diff) | [log](logs/M07-body.log.gz) |
|
||||
| M08-unknown | 1, killed | 16 / 2 | [diff](logs/M08-unknown.diff) | [log](logs/M08-unknown.log.gz) |
|
||||
| M09-ambiguous | 1, killed | 17 / 1 | [diff](logs/M09-ambiguous.diff) | [log](logs/M09-ambiguous.log.gz) |
|
||||
| M10-messaging | 1, killed | 17 / 1 | [diff](logs/M10-messaging.diff) | [log](logs/M10-messaging.log.gz) |
|
||||
| M11-cursor | 1, killed | 17 / 1 | [diff](logs/M11-cursor.diff) | [log](logs/M11-cursor.log.gz) |
|
||||
| M12-fence | 1, killed | 17 / 1 | [diff](logs/M12-fence.diff) | [log](logs/M12-fence.log.gz) |
|
||||
|
||||
## Final review scope and packet
|
||||
|
||||
Boyd reviews the three room cases, pure parser/export, README rows, new parser/room tests, and **the added read-only RUNNER_FILES mount for packages/bus/src/address.mjs**. mos-1567 authorizes that one sandbox source line and the sole old-test delta: project-runtime.test.mjs:17-18 adds exactly the Row265 comment and parser import pin (numstat 2 added / 0 deleted). Byte comparison against base after removing those additions proves all other old-test bytes unchanged. Removing the single RUNNER_FILES addition likewise restores sandbox.mjs to base. sandbox.test.mjs has no byte change. New address.test.mjs block proves the mounted parser source has no import word.
|
||||
|
||||
The widened changed-file hash-context audit includes sandbox.mjs: [pin-context-with-sandbox.txt](pin-context-with-sandbox.txt), zero 64-hex-near-reference hits. The original reader and positive-control receipts remain alongside it; the missed source pin and first STOP remain recorded, not concealed. No role/schema/tester allowlist or helper change ships. BUILD-LOG and SESSIONS only append tracking entries.
|
||||
|
||||
Base: `ec86f2c847f6154a0677c6ed23c87dabc2c665d0`. Branch: `goldie/row265`. MANIFEST.sha256 lists every changed source/test/tracking/evidence file except itself, with repository-relative paths, and is verified before commit/push. Raw first-red logs are retained separately from green reruns.
|
||||
|
||||
Evidence packaging: logs are stored losslessly as `.log.gz`; decompress with `gzip -cd`. Decompression was byte-compared with each raw run log before packing. This preserves the original failure/Node whitespace while keeping the committed diff whitespace-clean. The run driver recreates raw `.log` files when used. Mutant busy-check headers identify only their own Python driver PID3686656 as BUSY; positive runs report CLEAR. The driver held the assigned slot throughout, with serialized child tests.
|
||||
@@ -0,0 +1,51 @@
|
||||
996e9d0c1e7a8436691ce5d5d89b4eb940b07aafb3386f63a9847d203f43d998 BUILD-LOG.md
|
||||
26b5821895a152ddac66a26f16585c6c48ec71d99ca7e7c2b9a4d5a71943c5b4 agents/goldie/work/queue-265/BUILD.md
|
||||
1289c0f9f4e857590490a0972b5300cc8fb1a86e3ef79fe63e47472709e982f6 agents/goldie/work/queue-265/logs/01-address.log.gz
|
||||
bc0ac95454ca4a4478641832e68caeb82bfe8b1fdc9abc0b80f1126ef42b7be5 agents/goldie/work/queue-265/logs/02-rooms.log.gz
|
||||
459a45bd772576847338a438b27f4f578a2a660be744e1404fd239d192c6957c agents/goldie/work/queue-265/logs/03-bus.log.gz
|
||||
c30cecb149e98d64f285e81a82d07a0a89dad5ea223f9031b543230615663bb5 agents/goldie/work/queue-265/logs/M01-human.diff
|
||||
a75e02d91ff46fbfaf627beea943183b769e417bcb572bf11f778f0fca7fcf29 agents/goldie/work/queue-265/logs/M01-human.log.gz
|
||||
b78b1c5b205272411710f56a981acf58dc09c46333369924b98c4c8657dc949c agents/goldie/work/queue-265/logs/M02-tester.diff
|
||||
6a680dcb1db461cce3a3a2e066aa1245e4f84aa6754354058009bc0f6ebf4185 agents/goldie/work/queue-265/logs/M02-tester.log.gz
|
||||
02a6cfab404405099fc39eb83413614c67a0c4c3f87cd23c3849a35d1055ddb0 agents/goldie/work/queue-265/logs/M03-project.diff
|
||||
8b7cd0c568510e49b61fa14fce7b2680025b773eda884e0a84163d240a124e39 agents/goldie/work/queue-265/logs/M03-project.log.gz
|
||||
18b2cb43f2799301cf934e4c327bc1a03e08ff8d11ff590cb9677c86dfd43ff0 agents/goldie/work/queue-265/logs/M04-members.diff
|
||||
195f9ec57c9494552566d2f8b87f3b17a210b811f8b556dd65db89814e237816 agents/goldie/work/queue-265/logs/M04-members.log.gz
|
||||
a773e214c76acf5285ac4ca17cdd90dd552ddc79157abe75ef6de7f67a0b6a70 agents/goldie/work/queue-265/logs/M05-collision.diff
|
||||
1148cda3f6aaa7f6aeaab2f95c932114ddeac5bfd9318a6b77a4148079d486ed agents/goldie/work/queue-265/logs/M05-collision.log.gz
|
||||
56400065c9cdb6305763b2bff8c0e06e9ae1223fa89b4467634a9951622d3fbe agents/goldie/work/queue-265/logs/M06-open.diff
|
||||
b8f9bd9a5c488e604d0afe3feda4a2602359a94a49c7fdbd2f0f6a421b0ca1ce agents/goldie/work/queue-265/logs/M06-open.log.gz
|
||||
2c1b745a15602cdcaf194131e4d6e0f8fa349e9712ce2e8cb6e695699e4ec19d agents/goldie/work/queue-265/logs/M07-body.diff
|
||||
4857461e7a2ceb01a6446acb4708a732949a0d8d1a85fa2f6d10bedde6b9e11b agents/goldie/work/queue-265/logs/M07-body.log.gz
|
||||
e3480e69d016caea6c1fbcdfa022b18214a10e8deca4409d0dc2f72261d2ee92 agents/goldie/work/queue-265/logs/M08-unknown.diff
|
||||
3d0f347f21df3c388a4f39679d4694aadc9684ae8b8ad2d30bf0bb316e48dcb0 agents/goldie/work/queue-265/logs/M08-unknown.log.gz
|
||||
62420ed20affcab256facee05414b8b760a4c79a60f60be0c06c88abb61ec6ae agents/goldie/work/queue-265/logs/M09-ambiguous.diff
|
||||
14a18d61344df592e885c46ace34e0c59914f4fbe591fa99eeb3d81fa8ef8bf1 agents/goldie/work/queue-265/logs/M09-ambiguous.log.gz
|
||||
0671bf77394f82df92252275171fd9edea2e5b2e7143387840d03bda544676bf agents/goldie/work/queue-265/logs/M10-messaging.diff
|
||||
4202215d40999e0d1cc98ebfb5445f0c8fd4688e6ca968e85d225723076ca8a7 agents/goldie/work/queue-265/logs/M10-messaging.log.gz
|
||||
042598ecb551e761d37af898f69e17ec137d2f39687dbf81ce5e1eeb98a38202 agents/goldie/work/queue-265/logs/M11-cursor.diff
|
||||
deff3d0514c379bd3d9c6f6bbc60db59b9dce2baaafd6fa611711c98cc8365ce agents/goldie/work/queue-265/logs/M11-cursor.log.gz
|
||||
780e70fbcb2328006c8d9ace75aa62fcd0d935b4d758b50b0f5c5d5c55604122 agents/goldie/work/queue-265/logs/M12-fence.diff
|
||||
b629b41a39c028b70d4b847c351d248d6d70e8175d6d27f563b0ed8817f89642 agents/goldie/work/queue-265/logs/M12-fence.log.gz
|
||||
1b8c3cec31b44ff018a1bc74fef95996bcbdd21d340e2d2605154ba645648040 agents/goldie/work/queue-265/logs/rerun-00-sandbox.log.gz
|
||||
d18c5a9432de06ebec36c937da7dfb1a3e79dc397b33ff624058af351eee24dd agents/goldie/work/queue-265/logs/rerun-01-address.log.gz
|
||||
d10b83ccec56e47d8c7c74d8304fc4fc87b8ea92a50098380cd02b8012c32d09 agents/goldie/work/queue-265/logs/rerun-02-rooms.log.gz
|
||||
6ff5955fe5c85f51c1cf4daeb8eddd61ef1a30e170221f99bfb08da8304d7e4a agents/goldie/work/queue-265/logs/rerun-03-bus.log.gz
|
||||
eeba0182f06cee0c06cd15ecf7ef43aaeffd7149b2202cc0653078c20bb46198 agents/goldie/work/queue-265/logs/rerun-04-mutants-driver.log.gz
|
||||
5fe9fe35af60ef951c5c68b040aa9bb6067d9c746e143bb5782eb428acd4e4fe agents/goldie/work/queue-265/logs/rerun-05-restored-address.log.gz
|
||||
2b3f3305ff5f6926a0269bd6c88d5f2e4a89727eefcf99625bebb51a70abb649 agents/goldie/work/queue-265/logs/rerun-05-restored-rooms.log.gz
|
||||
f11ae40d923affad1936fe51d05981ac201a1051bc9f877d75fc1d544c6e38f1 agents/goldie/work/queue-265/mutants.py
|
||||
e9cdf16670a16a14f3dd722a69c63a1785e4deb3608ee05d519b0ec8c1a9ca0b agents/goldie/work/queue-265/pin-context-grep.txt
|
||||
491ba3bb25e918049249178f3c6a67b2b4f56a197f0a67059acb2420cd03b73c agents/goldie/work/queue-265/pin-context-with-sandbox.txt
|
||||
784fb42bff187c0eebcf7756942ec3cf56f8faa7b54ce4ad96181b81fe46f0e9 agents/goldie/work/queue-265/pin-positive-control.txt
|
||||
766ead519eefbde958bbdbf77bf858b10dd1e3d9dee8ab5d356fe821ce7602fe agents/goldie/work/queue-265/reader-grep.txt
|
||||
6c3c44eb943f33d5bb547f542c77da4d6f04e738056466bc491c2f6fd5f67946 docs/SESSIONS.md
|
||||
1ba33af1f42c88a5346d25fe915a44ee70a27fbc6b9ab880db0866cbdad5d45d packages/bus/README.md
|
||||
232a1c77c2f17011c53b55fc6052f70985598210769d41d7c2c0b535328aad23 packages/bus/src/address.mjs
|
||||
71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781 packages/bus/src/broker.mjs
|
||||
ed57d48d2af4a225259e0a44c86a30b12792ec9f725dd9c89251569b7dc89e5a packages/bus/src/index.mjs
|
||||
a6725ffe30d44981a9b9d3a3c319f7e96a46ec7bd74f0c8d869d9a798eca1a81 packages/bus/tests/address-cases.mjs
|
||||
fae431bc3743b224f69da33c4cebe376b47257d67f52d25d3ab0cb59f224a607 packages/bus/tests/address.test.mjs
|
||||
8594e94384c1c8b7234cfa8bbc17456cff86d9adef936cd1059a0d5a60e4546b packages/bus/tests/project-runtime.test.mjs
|
||||
892c3709393fa8962710bc07565d1097bbd79ea1fbfda34af5f9385db34ed0fa packages/bus/tests/rooms.test.mjs
|
||||
83019fa6b39ad9fd79b84627856b4dec82683d8261b8cba9b9ac840e0c489268 packages/seat/src/sandbox.mjs
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M01-human
|
||||
@@ -6423,7 +6423,6 @@
|
||||
case 'room.open':
|
||||
case 'room.send':
|
||||
case 'room.read': {
|
||||
- this.#human(s);
|
||||
const opening = verb === 'room.open';
|
||||
const sending = verb === 'room.send';
|
||||
keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'],
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
--- broker.original
|
||||
+++ M02-tester
|
||||
@@ -122,7 +122,8 @@
|
||||
// Row 175 (#1673, Q173): the test system's reads and two writes, name.set and project.create,
|
||||
// each recorded with `tester` as its actor. The organization branch keeps its own list below.
|
||||
// Row 177 (#1675, Q173): the tester's own threads, owner `tester`, and a stop of their runs.
|
||||
-export const TESTER_VERBS = Object.freeze(['message.send', 'message.receive', 'message.read', 'inbox', 'agents', 'tasks', 'trail', 'decisions', 'messages', 'replies',
|
||||
+export const TESTER_VERBS = Object.freeze([
|
||||
+ 'room.open', 'room.send', 'room.read','message.send', 'message.receive', 'message.read', 'inbox', 'agents', 'tasks', 'trail', 'decisions', 'messages', 'replies',
|
||||
'usage', 'names', 'limits', 'plugins', 'instances', 'project.list', 'thread.list', 'name.set', 'project.create', 'credentials',
|
||||
'thread.create', 'thread.open', 'thread.send', 'thread.messages', 'assistant.stop',
|
||||
// Row 194 (#1692, D4.1): launch and terminate, and the read of the requests they make.
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M03-project
|
||||
@@ -6429,7 +6429,6 @@
|
||||
keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'],
|
||||
sending ? ['project', 'body'] : ['project']);
|
||||
const project = identifier(a.project);
|
||||
- if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');
|
||||
const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort();
|
||||
if (!members.length) fail('thread-instance-missing');
|
||||
const owner = this.#owner(s);
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M04-members
|
||||
@@ -6431,7 +6431,6 @@
|
||||
const project = identifier(a.project);
|
||||
if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');
|
||||
const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort();
|
||||
- if (!members.length) fail('thread-instance-missing');
|
||||
const owner = this.#owner(s);
|
||||
// Preflight all collisions/missing threads before open creates or send writes anything.
|
||||
const threads = members.map((instance) => {
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
--- broker.original
|
||||
+++ M05-collision
|
||||
@@ -6436,8 +6436,6 @@
|
||||
// Preflight all collisions/missing threads before open creates or send writes anything.
|
||||
const threads = members.map((instance) => {
|
||||
const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance);
|
||||
- if (t && (t.instance !== instance || t.owner !== owner))
|
||||
- fail(opening ? 'room-thread-name-taken' : 'room-not-open');
|
||||
if (!t && !opening) fail('room-not-open');
|
||||
return t ?? { instance, name: instance };
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M06-open
|
||||
@@ -6438,7 +6438,6 @@
|
||||
const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance);
|
||||
if (t && (t.instance !== instance || t.owner !== owner))
|
||||
fail(opening ? 'room-thread-name-taken' : 'room-not-open');
|
||||
- if (!t && !opening) fail('room-not-open');
|
||||
return t ?? { instance, name: instance };
|
||||
});
|
||||
if (opening) {
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M07-body
|
||||
@@ -6450,7 +6450,6 @@
|
||||
if (sending) {
|
||||
string(a.body, 32768);
|
||||
const addressed = parseAddress(a.body);
|
||||
- if (!addressed.body.trim()) fail('invalid-request');
|
||||
const selected = new Set();
|
||||
for (const tag of addressed.tags) {
|
||||
const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance)));
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M08-unknown
|
||||
@@ -6454,7 +6454,6 @@
|
||||
const selected = new Set();
|
||||
for (const tag of addressed.tags) {
|
||||
const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance)));
|
||||
- if (!candidates.size) fail('unknown-addressee');
|
||||
if (candidates.size > 1) fail('addressee-ambiguous');
|
||||
selected.add([...candidates][0]);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M09-ambiguous
|
||||
@@ -6455,7 +6455,6 @@
|
||||
for (const tag of addressed.tags) {
|
||||
const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance)));
|
||||
if (!candidates.size) fail('unknown-addressee');
|
||||
- if (candidates.size > 1) fail('addressee-ambiguous');
|
||||
selected.add([...candidates][0]);
|
||||
}
|
||||
const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance));
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M10-messaging
|
||||
@@ -6462,7 +6462,6 @@
|
||||
const sessions = targets.map((t) => {
|
||||
const session = this.#defaultSession(t.id);
|
||||
this.#sessionFence(session);
|
||||
- this.#checkMessaging(s.business, 'human', t.instance);
|
||||
return session;
|
||||
});
|
||||
const at = this.#now();
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
--- broker.original
|
||||
+++ M11-cursor
|
||||
@@ -6488,7 +6488,7 @@
|
||||
`SELECT m.seq FROM messages m JOIN message_threads mt ON mt.message=m.id WHERE m.id=? AND m.business=? AND mt.thread IN (${placeholders})`,
|
||||
identifier(a.after), s.business, ...tids,
|
||||
)?.seq;
|
||||
- if (after === undefined) fail('message-not-found');
|
||||
+ if (after === undefined) after = 0;
|
||||
}
|
||||
// At most one row per instance joins a group; one lookahead row completes the last page.
|
||||
const rows = this.#store.all(
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
--- broker.original
|
||||
+++ M12-fence
|
||||
@@ -6461,7 +6461,6 @@
|
||||
const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance));
|
||||
const sessions = targets.map((t) => {
|
||||
const session = this.#defaultSession(t.id);
|
||||
- this.#sessionFence(session);
|
||||
this.#checkMessaging(s.business, 'human', t.instance);
|
||||
return session;
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Slot-only runner. Every mutation restores and verifies the complete broker bytes."""
|
||||
from pathlib import Path
|
||||
import hashlib, os, re, subprocess, sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
SOURCE = ROOT / 'packages/bus/src/broker.mjs'
|
||||
OUT = Path(__file__).resolve().parent / 'logs'
|
||||
MUTANTS = [
|
||||
('M01-human', 'human-required', 'room verbs require the human', ' this.#human(s);\n', ''),
|
||||
('M02-tester', 'tester-refused', 'room verbs require the human', None, None),
|
||||
('M03-project', 'project-not-found', 'room verbs refuse missing projects', " if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');\n", ''),
|
||||
('M04-members', 'thread-instance-missing', 'room verbs refuse missing projects', " if (!members.length) fail('thread-instance-missing');\n", ''),
|
||||
('M05-collision', 'room-thread-name-taken', 'room.open preflights instance collisions', " if (t && (t.instance !== instance || t.owner !== owner))\n fail(opening ? 'room-thread-name-taken' : 'room-not-open');\n", ''),
|
||||
('M06-open', 'room-not-open', 'send/read refuse a missing canonical member', " if (!t && !opening) fail('room-not-open');\n", ''),
|
||||
('M07-body', 'invalid-request', 'unknown or non-member addressees and tags-only bodies', " if (!addressed.body.trim()) fail('invalid-request');\n", ''),
|
||||
('M08-unknown', 'unknown-addressee', 'unknown or non-member addressees and tags-only bodies', " if (!candidates.size) fail('unknown-addressee');\n", ''),
|
||||
('M09-ambiguous', 'addressee-ambiguous', 'a legacy name colliding with another member key', " if (candidates.size > 1) fail('addressee-ambiguous');\n", ''),
|
||||
('M10-messaging', 'message-sender-refused', 'messaging denial on the last target', " this.#checkMessaging(s.business, 'human', t.instance);\n", ''),
|
||||
('M11-cursor', 'message-not-found', 'room.read groups one send, pages', " if (after === undefined) fail('message-not-found');\n", ' if (after === undefined) after = 0;\n'),
|
||||
('M12-fence', 'session-clearing', 'a clearing last member fences the whole send', ' this.#sessionFence(session);\n', ''),
|
||||
]
|
||||
|
||||
def sha(data): return hashlib.sha256(data).hexdigest()
|
||||
|
||||
def main():
|
||||
OUT.mkdir(exist_ok=True)
|
||||
original = SOURCE.read_bytes()
|
||||
text = original.decode()
|
||||
start = text.index(" case 'room.open':")
|
||||
end = text.index(" // A thread belongs", start)
|
||||
block = text[start:end]
|
||||
for name, refusal, expected, old, new in MUTANTS:
|
||||
if old is None:
|
||||
needle = 'export const TESTER_VERBS = Object.freeze(['
|
||||
assert text.count(needle) == 1
|
||||
changed = text.replace(needle, needle + "\n 'room.open', 'room.send', 'room.read',", 1)
|
||||
else:
|
||||
assert block.count(old) == 1, (name, block.count(old))
|
||||
changed = text[:start] + block.replace(old, new, 1) + text[end:]
|
||||
# Save conventional original→mutant diff without relying on shell interpolation.
|
||||
import difflib
|
||||
(OUT / f'{name}.diff').write_text(''.join(difflib.unified_diff(text.splitlines(True), changed.splitlines(True), fromfile='broker.original', tofile=name)))
|
||||
tmp = subprocess.check_output(['mktemp', '-d', '/mnt/storage/scratch/tmp/XXXXXX'], text=True).strip()
|
||||
env = {**os.environ, 'TMPDIR': tmp}
|
||||
log = OUT / f'{name}.log'
|
||||
try:
|
||||
SOURCE.write_text(changed)
|
||||
with log.open('w') as stream:
|
||||
for command in [['date', '-u'], ['uptime'], ['bash', '-c', 'bash /mnt/storage/scratch/sage-busy-now.sh 2>/dev/null | tail -1']]:
|
||||
subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, check=True)
|
||||
stream.write(f'TMPDIR={tmp}\nmutant={name} refusal={refusal} original_sha256={sha(original)}\n')
|
||||
stream.flush()
|
||||
result = subprocess.run(['timeout', '-k', '10', '120', 'node', '--test', '--test-reporter=tap', 'packages/bus/tests/rooms.test.mjs'],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT)
|
||||
output = log.read_text()
|
||||
killed = result.returncode == 1 and re.search(r'(?m)^not ok \d+ - ' + re.escape(expected), output)
|
||||
print(f'{name}: exit={result.returncode} target_killed={bool(killed)} log={log}', flush=True)
|
||||
if not killed: return 1
|
||||
finally:
|
||||
SOURCE.write_bytes(original)
|
||||
assert SOURCE.read_bytes() == original
|
||||
assert sha(SOURCE.read_bytes()) == sha(original)
|
||||
with log.open('a') as stream: stream.write(f'RESTORED sha256={sha(original)}\n')
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__': sys.exit(main())
|
||||
@@ -0,0 +1 @@
|
||||
No changed-file mention has a literal 64-hex digest within eight lines.
|
||||
@@ -0,0 +1 @@
|
||||
No changed-product-file mention has a literal 64-hex digest within eight lines.
|
||||
@@ -0,0 +1,6 @@
|
||||
packages/bus/src/store.mjs:15:const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8');
|
||||
packages/bus/src/store.mjs:16:export const SCHEMA_SHA256 = 'a2ba3a46f12e56139ecae9e55227d00847842aedec50c1d92aaae28844ca48e7';
|
||||
packages/bus/src/store.mjs:106:const hex = (value) => createHash('sha256').update(value).digest('hex');
|
||||
packages/bus/src/store.mjs:107:if (hex(ddl) !== SCHEMA_SHA256) throw Error('schema-source-mismatch');
|
||||
packages/bus/tests/schema.test.mjs:17: db.exec(readFileSync(new URL('../schema.sql', import.meta.url), 'utf8'));
|
||||
packages/bus/tests/schema.test.mjs:90: const hex = (s) => createHash('sha256').update(s).digest('hex');
|
||||
@@ -0,0 +1,133 @@
|
||||
packages/bus/tests/session-compactions.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/console-decide.test.mjs:14:import { Broker, CONSOLE_VERBS } from '../src/broker.mjs';
|
||||
packages/harness/tests/runner.test.mjs:14:import { Broker, BusError } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/project-files.test.mjs:8:import { PROJECT_FILE_VERBS } from '../src/broker.mjs';
|
||||
packages/cli/tests/lifecycle.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/lifecycle-verbs.test.mjs:11:import { Broker, LIFECYCLE_REQUEST_MS, TESTER_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/lifecycle-verbs.test.mjs:74: for (const verb of ['instance.launch', 'instance.end', 'instance.request']) assert.ok(TESTER_VERBS.includes(verb), verb);
|
||||
packages/bus/tests/lifecycle-verbs.test.mjs:75: assert.equal(TESTER_VERBS.includes('instance.regenerate'), false);
|
||||
packages/bus/tests/tasks.test.mjs:9:import { Broker, BusError, TASK_VERBS } from '../src/broker.mjs';
|
||||
packages/harness/tests/tools.test.mjs:4:import { ORGANIZATION_KEYS as BROKER_ORGANIZATION_KEYS, TASK_PAGE, TASK_PAGE_MAX } from "../../bus/src/broker.mjs";
|
||||
packages/seat/tests/session.test.mjs:11:import { Broker } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/decision-route.test.mjs:12:import { Broker } from '../src/broker.mjs';
|
||||
packages/cli/tests/thread-launch.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/seat/tests/sandbox.test.mjs:15:import { Broker } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/host-run.test.mjs:16:import { Broker, ORGANIZATION_KEYS } from '../src/broker.mjs';
|
||||
packages/bus/tests/host-run.test.mjs:181: // organization.set reaches the writer as limits.set (broker.mjs:1379).
|
||||
packages/cli/tests/helpers.mjs:10:import { BusError, Broker } from "../../bus/src/broker.mjs";
|
||||
packages/tasks/tests/fields.test.mjs:10:import { TASK_VERBS } from '../../bus/src/broker.mjs';
|
||||
packages/tasks/tests/fields.test.mjs:226: // decision back to that role (broker.mjs, decision.raise).
|
||||
packages/webui/tests/instances.test.mjs:15:import { BusError } from '../../bus/src/broker.mjs';
|
||||
packages/cli/tests/tester-threads.test.mjs:14:import { BusError, Broker } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/staff-request.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/tasks/tests/world.mjs:5:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/cli/tests/processing-cli.test.mjs:7:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/grant-host.test.mjs:10:import { Broker, registrySha256 } from '../src/broker.mjs';
|
||||
packages/business/tests/workflow-roles.test.mjs:115: // Q169 (broker.mjs END_PROTECTED, #hardWait): revoking the assistant is a hard wait as well.
|
||||
packages/business/tests/workflow-roles.test.mjs:136: // sage-0709: only recordLand spends a decision (broker.mjs #spent). checkLand refuses decision-consumed after
|
||||
packages/cli/tests/thread-revoke.test.mjs:18:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/tester-writes.test.mjs:10:import { Broker, TESTER_VERBS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/tester-writes.test.mjs:18:test('row 175: TESTER_VERBS and the organization branch tester set are pinned', () => {
|
||||
packages/bus/tests/tester-writes.test.mjs:19: assert.deepEqual([...TESTER_VERBS], [
|
||||
packages/bus/tests/tester-writes.test.mjs:32: assert.ok(Object.isFrozen(TESTER_VERBS) && Object.isFrozen(TESTER_ORGANIZATION_VERBS));
|
||||
packages/cli/tests/stack-release.test.mjs:1993: for(const [f,text] of [['packages/cli/src/cli.mjs',id===A?'// fixture host from before row 163\nprocess.exit(1);\n':script(rHost)],['packages/webui/src/cli.mjs',script(rConsole)],['packages/bus/src/store.mjs',rStore(v,froms)],['packages/bus/src/migrate-schema.mjs',script(rMigrate)],['packages/bus/src/broker.mjs','export class BusError extends Error { constructor(code) { super(code); this.code = code; } }\n']]){mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text)}
|
||||
packages/cli/tests/session-cli.test.mjs:94: const { Broker } = await import('../../bus/src/broker.mjs');
|
||||
packages/cli/tests/session-launch.test.mjs:21:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/cli/tests/revoke-window.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/chief-of-staff.test.mjs:10:import { Broker, ACTIONS } from '../src/broker.mjs';
|
||||
packages/cli/tests/instances.test.mjs:13:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/cli/tests/thread-cli.test.mjs:14:import { BusError, Broker } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/processing.test.mjs:6:import { Broker } from '../src/broker.mjs';
|
||||
packages/cli/tests/idle-stop-race.test.mjs:17:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/cli/tests/tester-reads.test.mjs:12:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/cli/tests/session-clear.test.mjs:18:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/names.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/cli/tests/compactions-launch.test.mjs:24:import { BusError } from "../../bus/src/broker.mjs";
|
||||
packages/cli/tests/tester.test.mjs:7:import { BusError, Broker } from "../../bus/src/broker.mjs";
|
||||
packages/bus/tests/reload.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/agents.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/end-launch.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/webui/tests/decide-browser.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/credential-verbs.test.mjs:11:import { Broker, CREDENTIAL_VERBS } from '../src/broker.mjs';
|
||||
packages/webui/tests/send-browser.test.mjs:13:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/launch-notice.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/messages.test.mjs:3:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/workflow-roles.test.mjs:11:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/library-sync.test.mjs:11:import { Broker, LIBRARY_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/credential-status.test.mjs:12:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/gitea-grant-docker.test.mjs:15:import { Broker, GRANT_EVIDENCE } from '../src/broker.mjs';
|
||||
packages/bus/tests/thread-migration.test.mjs:11:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/comment-cursors.test.mjs:7:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/thread-claims.test.mjs:11:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/state.test.mjs:7:import { Broker, TASK_PAGE } from '../src/broker.mjs';
|
||||
packages/bus/tests/access-revoke.test.mjs:14:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/plugins-verb.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/transport.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/publish.test.mjs:14:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/publish.test.mjs:221: const { ACTIONS, HARD_WAITS } = await import('../src/broker.mjs');
|
||||
packages/bus/tests/tester-threads.test.mjs:11:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/tester-threads.test.mjs:284: // The row #specNotice (broker.mjs) writes for a human save of the spec in project stack. The save
|
||||
packages/bus/tests/approval-binding.test.mjs:7:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/fire-revokes-grants.test.mjs:15:import { Broker, GRANT_EVIDENCE, registrySha256 } from '../src/broker.mjs';
|
||||
packages/bus/tests/daily-quota-runtime.test.mjs:10:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/limit-checks.test.mjs:323: for (const file of ['../src/runtime.mjs', '../src/broker.mjs', '../src/process.mjs']) {
|
||||
packages/bus/tests/spec-fixtures.mjs:6:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/decisions.test.mjs:9:import { Broker, RECENT } from '../src/broker.mjs';
|
||||
packages/bus/tests/peer-transport.test.mjs:9:import { BusError } from '../src/broker.mjs';
|
||||
packages/bus/tests/peer-transport.test.mjs:47: const {TASK_VERBS,BusError}=await import('../src/broker.mjs');
|
||||
packages/bus/tests/messaging.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/rooms.test.mjs:8:import { Broker, THREAD_PAGE, THREAD_PAGE_MAX, TESTER_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/rooms.test.mjs:80: assert.ok(!TESTER_VERBS.includes(verb));
|
||||
packages/bus/tests/web.test.mjs:8:import { Broker, BusError, WEB_UNTRUSTED } from '../src/broker.mjs';
|
||||
packages/bus/tests/replies.test.mjs:11:import { Broker, REPLIES } from '../src/broker.mjs';
|
||||
packages/bus/tests/thread-shares.test.mjs:14:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/message-citation.test.mjs:6:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/limits.test.mjs:7:import {Broker} from '../src/broker.mjs';
|
||||
packages/bus/tests/host-fixtures.mjs:6:import { registrySha256, writeGrantEvent } from '../src/broker.mjs';
|
||||
packages/bus/tests/gitea-grant.test.mjs:12:import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs';
|
||||
packages/bus/tests/csuite-messaging.test.mjs:12:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/decision-close.test.mjs:9:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/grant-scope.test.mjs:14:import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs';
|
||||
packages/bus/tests/project-await.test.mjs:7:import { Broker, BusError } from '../src/broker.mjs';
|
||||
packages/bus/tests/launcher-released.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/limit-stop.test.mjs:12:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/message-filter.test.mjs:12:import { Broker, REPLIES } from '../src/broker.mjs';
|
||||
packages/bus/tests/land.test.mjs:18:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/single-use.test.mjs:7:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/session-clear.test.mjs:10:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/grant-migration.test.mjs:10:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/usage-broker.test.mjs:7:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/project-runtime.test.mjs:13: const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8');
|
||||
packages/bus/tests/task-read.test.mjs:7:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/library.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/instances-verb.test.mjs:11:import { Broker, LIFECYCLE_HUMAN_VERBS, ORGANIZATION_KEYS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/routes-verb.test.mjs:13:import { Broker, ORGANIZATION_KEYS } from '../src/broker.mjs';
|
||||
packages/bus/tests/broker.test.mjs:10:const { Broker } = await import('../src/broker.mjs').catch((e) => {
|
||||
packages/bus/tests/grant-enroll.test.mjs:10:import { Broker, BusError, writeGrantEvent, registrySha256 } from '../src/broker.mjs';
|
||||
packages/bus/tests/hard-waits.test.mjs:7:import { Broker, ACTIONS, HARD_WAITS } from '../src/broker.mjs';
|
||||
packages/bus/tests/requirements.test.mjs:10:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/grant-executor.test.mjs:15:import { Broker, registrySha256, writeGrantEvent } from '../src/broker.mjs';
|
||||
packages/bus/tests/sessions.test.mjs:10:import { Broker } from '../src/broker.mjs';
|
||||
packages/bus/tests/user-principal.test.mjs:14: Broker, ACTIONS, USER_VERBS, TESTER_VERBS, TASK_VERBS, WEB_VERBS, HOST_VERBS, HOSTS_VERBS, CREDENTIAL_VERBS,
|
||||
packages/bus/tests/user-principal.test.mjs:16:} from '../src/broker.mjs';
|
||||
packages/bus/tests/user-principal.test.mjs:137: ...ACTIONS, ...TESTER_VERBS, ...TESTER_ORGANIZATION_VERBS, ...PROJECT_FILE_VERBS, ...LIBRARY_VERBS, ...CREDENTIAL_VERBS, ...HOSTS_VERBS,
|
||||
packages/bus/tests/tester.test.mjs:10:import { Broker, TASK_VERBS } from '../src/broker.mjs';
|
||||
packages/bus/tests/threads.test.mjs:9:import { Broker, THREAD_PAGE } from '../src/broker.mjs';
|
||||
packages/webui/tests/routes-browser.test.mjs:15:import {BusError} from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/creative.test.mjs:8:import { Broker } from '../src/broker.mjs';
|
||||
packages/webui/tests/granted-user.test.mjs:16:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/task-page.test.mjs:7:import { Broker, TASK_PAGE, TASK_PAGE_MAX } from '../src/broker.mjs';
|
||||
packages/webui/tests/user-option-swap.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/webui/tests/board-held.test.mjs:20:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/webui/tests/chat-sessions-access.test.mjs:10:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/review.test.mjs:14:import { Broker } from '../src/broker.mjs';
|
||||
packages/webui/tests/sso-browser.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/revoke-window.test.mjs:13:import { Broker } from '../src/broker.mjs';
|
||||
packages/webui/tests/bus.test.mjs:13:import { Broker } from '../../bus/src/broker.mjs';
|
||||
packages/bus/tests/viewer.test.mjs:13: Broker, ACTIONS, USER_VERBS, VIEWER_VERBS, BOARD_VERBS, TESTER_VERBS, TESTER_ORGANIZATION_VERBS, PROJECT_FILE_VERBS,
|
||||
packages/bus/tests/viewer.test.mjs:14:} from '../src/broker.mjs';
|
||||
packages/bus/tests/viewer.test.mjs:116: const others = new Set([...ACTIONS, ...TESTER_VERBS, ...TESTER_ORGANIZATION_VERBS, ...PROJECT_FILE_VERBS, ...BOARD_VERBS, 'no.such.verb']);
|
||||
packages/webui/tests/tester-fixture.mjs:12:import { Broker, BusError } from '../../bus/src/broker.mjs';
|
||||
packages/webui/tests/routes.test.mjs:12:import { BusError } from '../../bus/src/broker.mjs';
|
||||
packages/webui/tests/chat.test.mjs:191: const { Broker } = await import('../../bus/src/broker.mjs');
|
||||
packages/webui/tests/bus-browser.test.mjs:11:import { Broker } from '../../bus/src/broker.mjs';
|
||||
@@ -0,0 +1,56 @@
|
||||
# Row 326 / #1822 — five run-sheet gaps
|
||||
|
||||
Goldie, 2026-10-24. Branch goldie/row-run-sheet in /home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row-run-sheet. Base local refactor4ec279dc43922b338a94718a2afdaba87ef89c15, explicitly assigned by sage-3893. Brief docs/plans/2026-10-24_run-sheet-doc-brief.md at4ec279dc4, blobce3e24fa, section Brief: five run-sheet gaps. Reviewer Gladstone; Sage posts review and owns landing/full gate.
|
||||
|
||||
## Before / after and choices
|
||||
|
||||
Before: the sheet omitted proof bytes/newline handling, treated render refusal without distinguishing metadata/content, omitted missing-project warning consequences and a pre-install key check, offered no first-install retry form, and omitted the bus-relative backup checksum form.
|
||||
|
||||
After: the sheet covers all five cases in their existing sections6/9. Numbered section headings are unchanged. Only this BUILD.md and docs/guides/host-run-sheet.md are edited, per the brief's explicit two-file boundary. Session/build-phase registration and the manifest are recorded here rather than adding changes to SESSIONS.md, BUILD-LOG.md or a third manifest file. No host action or credential read. No suite or product command executed, and no CLEAR needed for this doc-only row (decision137).
|
||||
|
||||
| Gap / claim | Source read and how it supports the text |
|
||||
| --- | --- |
|
||||
| Proof format, optional LF | webui/src/serve.mjs:257-267 opens no-follow, checks regular/uid/mode600-or400, strips exactly one trailing LF at263, then requires32+ ASCII base64url chars at264. Table states the allowed alphabet and one optional trailing LF. Extra newline/CR is outside that alphabet. |
|
||||
| Render versus start | case2-unit.sh:397-408 explicitly stats/realpaths the proof and never opens it. webui/src/serve.mjs:267 throws the content error; webui/src/cli.mjs:138-140 catches and sets exit2. Table now distinguishes metadata refusal at render from bad bytes at Console start. |
|
||||
| Missing project warning | business/src/cli.mjs:74-78 reports absent plus the no-project-variables warning, then continues. case2-install.sh:217-221 captures validate output and prints business-validates on success. Text makes that incomplete install possible and tells operators to stop on the missing-file warning, per Duckworth row323 N2. |
|
||||
| Pre-install key check | business/src/cli.mjs:71-82 checks every file-backed project, skips kind library, and requires loaded id==business key at80. Read-only inline Node uses only builtins and section2's pinned node, as mosaic. It reads the exact intended business input and files under each project's root, prints a match, and exits nonzero with STOP for read/JSON/id failure. Expected .53 output comes from .mosaic/project.json:3 and case1-business.mosaic-stack.json:10-11. Fixing business rather than project id is mos-1563. |
|
||||
| Why check before install | case2-install.sh:178 makes the copy before validation at217. New check is explicitly before case2-install.sh and needs no existing Mosaic config, npm modules or installed wrapper. |
|
||||
| Refused first-install residue | case2-install.sh:172/174 creates/bootstraps the config; bootstrap.sh:13 dispatches to mosaic-config.mjs, whose412-424 creates config.json or validates an existing one without rewriting it. case2-install.sh:200-211 may create businesses/file and library slots. case2-persona-import.mjs:300-308 may create library personas. Validation cleanup at case2-install.sh:217-219 removes only this run's new business file, retaining dirs. Copy may already exist under the path at install:80/178; stack-copy.sh:68 reuses an existing copy. |
|
||||
| Retry paths / refs | Mos-1565/ops-0291, as pinned in the brief, names exactly root checkout, mosaic clone and default config dir to move aside. UTC-suffixed mv is root-only documentation; no delete, chown or chmod. Destination collision/move failure prints STOP. Retry starts section6's first-install block again. The existing upgrade fetch targets only refs/remotes/origin/refactor; stack-copy.sh:44-48 requires reachability from both it and refs/heads/refactor. |
|
||||
| Credential re-placement | Existing sheet section9 names claude-session.env, library proof and library-gitea with owner/mode. Moving config aside preserves them but the new config needs Jason to re-place any already-filled files, under mos-1565. Existing per-agent slots still require credentials issue/Approve, never hand placement. No value or command to read a credential added. |
|
||||
| Backup hand check | stack-release.sh:475-482 creates activation/bus and writes SHA256SUMS from inside bus. restore at502 and rehearsal at2244 cd bus and check ../SHA256SUMS. New hand command does the same as mosaic, without --quiet so the expected bus.sqlite:OK is visible; ops-0292 records why checking in activation itself fails. |
|
||||
|
||||
## Read-only grep gate
|
||||
|
||||
Executed after editing, with no test/suite run:
|
||||
|
||||
```
|
||||
git grep -n host-run-sheet -- 'packages/*/tests/*' 'scripts/*'
|
||||
# no output, exit1
|
||||
|
||||
git grep -n case1-business.mosaic-stack.json -- 'packages/*/tests/*' 'scripts/*'
|
||||
packages/business/tests/case1-business-file.test.mjs:13:const DOCUMENT = join(REPO, "docs/guides/case1-business.mosaic-stack.json");
|
||||
packages/business/tests/case1-project-key.test.mjs:9:const CASE1 = join(REPO, "docs/guides/case1-business.mosaic-stack.json");
|
||||
# exit0: positive control, same command form, both tests read this document
|
||||
|
||||
git grep -n -E '(host-run-sheet.*[0-9a-f]{64}|[0-9a-f]{64}.*host-run-sheet)' -- 'packages/*/tests/*'
|
||||
# no output, exit1: no 64-hex test pin beside the sheet name
|
||||
```
|
||||
|
||||
An earlier positive control with case2-install.sh also found the real installer spawn in case1-business-file.test.mjs:39 and the CLI installer tests. The sheet-name search finds no reference at all in tests/scripts, stronger than just finding no same-line pin. No read/pin blocker.
|
||||
|
||||
Static syntax only: extracted the three new shell snippets, replaced only the version placeholder for parsing, and ran bash-n3/3. Extracted Node heredoc parsed with node --check --input-type=module. No snippet body executed. git diff --check passes. Source citations checked against this base; no new em dash and section numbering unchanged.
|
||||
|
||||
## Session / claimant receipt
|
||||
|
||||
2026-10-24 | Goldie a13dd6d5-5e73-4b7d-8318-02abeff24940 | Row326/#1822 doc-only build | Five gaps documented with source lines and grep control; no suites/host/credential action. Claim goldie-0326-start-1 rev3793, briefed→in-progress. In-review follows the pushed candidate; expected tokenless pre-send failure will be handed to Sage.
|
||||
|
||||
## Inline manifest
|
||||
|
||||
Only the sheet needs a content pin; BUILD.md is this receipt in the same candidate commit. An inline manifest honors the brief's two-file limit:
|
||||
|
||||
```
|
||||
e4c14516edff3bfbc81ea553a5a6ccebb6154942473540659d4681c2d56c9711 docs/guides/host-run-sheet.md
|
||||
```
|
||||
|
||||
Reviewer validates the five source chains and operational order. Sage's landing gate owns suites. No code row265 work started; its brief comes separately.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Gyro
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under Q246 and mos-1571, as the W2 brief names. Owns row 310, O2 W2:
|
||||
the WordCraft source import and reproducible restricted web build (docs/plans/2026-10-21_q195-writing-editor.md,
|
||||
"W2 WordCraft source import and reproducible restricted web build").
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `gyro/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and give the Opus 5.5 landing review (mos-1571). Registered by
|
||||
Sage on 2026-10-24 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Row 208 / 193d — Console shared threads and viewer reads
|
||||
|
||||
Base ec86f2c84, branch poe/row208; Gitea #1706. Approved design: e2d161e2e,
|
||||
sha256 dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523.
|
||||
|
||||
## Before
|
||||
|
||||
FED R6 supplies per-session user transport and the user-only page, row 192 SSO,
|
||||
and row 206 supplies schema 3l shares and bus refusals. No bus/schema changes needed.
|
||||
The webui channel adapter must forward the three existing share verbs.
|
||||
|
||||
## Choices and acceptance
|
||||
|
||||
Use the existing chat HTTP write fences and per-session transport for shares.
|
||||
Expose only live collaborator grant project/instance scopes as UI hints, never
|
||||
identity or bus authority. Re-read hints when rendering a user page; mixed grants
|
||||
must hide writes on viewer-only threads even when another project allows writes.
|
||||
Shared threads always read without session management, composer, resume or sharing.
|
||||
Keep the exact governance notice on user thread/share pages. Targets are typed
|
||||
owner/user principal names or *, avoiding a new user directory read API.
|
||||
|
||||
Add new isolated HTTP/broker and browser coverage. Remove each UI guard in mutants
|
||||
and show the real broker still refuses viewer create, send, open and share writes.
|
||||
Every test execution requires Sage CLEAR; use scratch TMPDIR, deadlines, npm ci,
|
||||
and MOSAIC_REQUIRE_EDITOR_BUILD=1. No live state or network destinations.
|
||||
|
||||
## Tracking
|
||||
|
||||
Queue next from linked tree refused as designed; canonical next matched pinned brief.
|
||||
Claim poe-0001 initially refused on Sage's queue lock; retry the same operation id.
|
||||
|
||||
Claim poe-0001 succeeded at canonical queue revision 3807 (in-progress).
|
||||
During implementation, three string replacements briefly targeted canonical
|
||||
packages/webui/src/public/{bus.js,user.js,chat.js} due to an incorrect command cwd.
|
||||
Checked the complete diffs, surgically reversed exactly those replacements, and
|
||||
applied them to this worktree. Sage independently confirmed canonical packages/
|
||||
has no remaining diff (sage-3914). No tests or live actions occurred there.
|
||||
|
||||
poe-0002 requested the test slot. sage-3914 approved the exact serialized plan,
|
||||
but CLEAR is pending Bentina returning her slot. No node/npm/browser commands
|
||||
before CLEAR. New tests are thread-sharing.test.mjs with thread-sharing-fixture.mjs;
|
||||
mutants.mjs runs four served-source mutants with assertion kills plus bus bypass.
|
||||
|
||||
## First verification (sage-3917 CLEAR)
|
||||
|
||||
Scratch logs: /mnt/storage/scratch/tmp/YU1G5B/.
|
||||
Syntax 12/12 exit 0; npm-ci.log exit 0; editor-build.log exit 0;
|
||||
thread-sharing.tap 7/7 exit 0. mutants.log and poe-208-mutants.json: four
|
||||
absence-test assertion kills (exit 1 each), four bus bypass proofs (exit 0 each),
|
||||
runner exit 0. Each mutant's individual log is poe-208-NAME-kill.tap or -bypass.tap.
|
||||
Full serialized webui.tap stopped on an existing-test red: 44 pass, 1 fail before
|
||||
termination; runner exit 1. Plugins not run. Slot returned to Sage via poe-0003.
|
||||
|
||||
Existing failure: chat-browser.test.mjs:123, uncertain writes/read failure draft
|
||||
preservation, line 133 expects #chat-body after no-bus-host. The new current.thread
|
||||
is not retained before list reads succeed, unlike old current.threads. A transient
|
||||
read failure therefore hides the existing owner conversation. Proposed fix retains
|
||||
same-thread metadata across refresh, never stale user grant scopes. No old tests
|
||||
changed and no further execution pending Sage's ruling/CLEAR.
|
||||
|
||||
## Approved correction (sage-3918)
|
||||
|
||||
The old test is correct; this change broke its transient-failure draft contract.
|
||||
Retain current.thread and shared only when the old id equals the requested id.
|
||||
User chatGrants still start empty and are read fresh, so a stale hint is never
|
||||
carried forward. A real 403/404 clears thread/shared metadata plus cached history,
|
||||
drafts and operation forms. chat-browser.test.mjs remains byte-identical.
|
||||
Fix applied statically; Sage will CLEAR the four mutants, full webui and plugins
|
||||
suites after Bentina returns the slot. No node/npm/browser commands before then.
|
||||
|
||||
Clarification during static review: retain the existing distinction between a
|
||||
thread-level refusal and a local bad-session selector (old row 272 B1 test).
|
||||
All 403/404 responses clear visible thread/shared metadata, as before; thread-level
|
||||
thread-not-owned/thread-not-found/access-refused also erase sibling cached history
|
||||
and drafts. A bad-session selector never erases valid sibling drafts/recovery keys.
|
||||
This preserves the existing invalidation contract rather than broadening the fix.
|
||||
|
||||
sage-3920 CLEAR: first mutant rerun stopped on a new-test failure, not a valid
|
||||
mutant kill. /mnt/storage/scratch/tmp/8UlAhS/poe-208-composer-kill.tap shows null
|
||||
old.thread on initial /chat: old?.id and undefined route id compared equal.
|
||||
Fixed by requiring old to exist before either same-id metadata carry. This is a
|
||||
new defect in the approved correction, no old test change. Rerunning the plan.
|
||||
|
||||
## Second slot outcome (sage-3920)
|
||||
|
||||
Scratch /mnt/storage/scratch/tmp/8UlAhS/.
|
||||
Initial mutants.log exit 1 was the new null-previous-state defect above. Fixed;
|
||||
mutants-r2.log exit 0: all four assertion kills exit 1, all four bypass proofs
|
||||
exit 0 (individual logs overwritten by the successful rerun; summary JSON kept).
|
||||
Full serialized webui.tap stopped on old tests: 114 top-level pass, 5 fail,
|
||||
runner exit 1. The original draft-preservation failure and row 272 B1 both pass.
|
||||
The new old-test failure is environmental: decide-sso.test.mjs cannot import
|
||||
root dependency oauth4webapi (ERR_MODULE_NOT_FOUND); all five failures share it.
|
||||
packages/webui npm ci installs its editor tooling, not root runtime dependencies.
|
||||
No tests changed; plugins not run. Request root npm ci from pinned lockfile in
|
||||
next slot before rerunning webui and plugins. Slot returned via poe-0004.
|
||||
|
||||
sage-3924 ruled the second red an environment gap, not a product defect. Approved
|
||||
root npm ci --ignore-scripts from the pinned lock, full webui then plugins,
|
||||
serialized. sage-3925 CLEAR starts that sequence. Push only after green; send
|
||||
base/tip and every changed path's SHA-256 to Sage and Gladstone.
|
||||
|
||||
## Final builder verification (sage-3925 CLEAR)
|
||||
|
||||
Root npm ci --ignore-scripts exit 0 from pinned lock.
|
||||
Full webui: exit 0, 418 pass, 0 fail, 0 skipped (271964 ms).
|
||||
Plugins: exit 0, 119 pass, 0 fail, 0 skipped (823 ms).
|
||||
Logs /mnt/storage/scratch/tmp/Et9xAa/{root-npm-ci.log,webui.tap,plugins.tap}.
|
||||
Slot returned immediately after plugins via poe-0005. All new acceptance cases
|
||||
are included in the full webui run. Four latest UI mutants and bus bypass proofs
|
||||
remain green at /mnt/storage/scratch/tmp/8UlAhS/mutants-r2.log and
|
||||
poe-208-mutants.json. No old tests changed. No bus authority/schema changes.
|
||||
|
||||
Acceptance: owner and collaborator can share/unshare own writable threads;
|
||||
shared lists preserve the plain-list shape and show owner names safely as text;
|
||||
shared threads cannot compose, resume, mutate sessions or reshare. Viewer old
|
||||
own threads retain read history with no write controls. Mixed grants and live
|
||||
downgrades hide writes only where coverage requires; revoked shares clear the
|
||||
view. Direct browser POSTs bypass all UI checks and the real broker still
|
||||
refuses viewer create/send/open/share. Existing exact governance notice retained.
|
||||
Global scripts/test-*.sh gate and independent reviewer approval remain Sage's
|
||||
landing gate; this builder does not claim those or any live deployment.
|
||||
@@ -0,0 +1,28 @@
|
||||
// Run only under Sage's CLEAR, with scratch TMPDIR and MOSAIC_REQUIRE_EDITOR_BUILD=1.
|
||||
// Mutants alter only the fixture-served copy of chat.js; repository source is untouched.
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const file = 'packages/webui/tests/thread-sharing.test.mjs';
|
||||
const results = [];
|
||||
for (const mutant of ['composer', 'open', 'share', 'create']) {
|
||||
for (const [stage, pattern, expected] of [
|
||||
['kill', '193d viewer UI has no', 1],
|
||||
['bypass', '193d bypassed UI checks', 0],
|
||||
]) {
|
||||
const run = spawnSync(process.execPath, ['--test', '--test-concurrency=1', '--test-name-pattern=' + pattern, file], {
|
||||
env: { ...process.env, POE_UI_MUTANT: mutant }, encoding: 'utf8', timeout: 90000,
|
||||
});
|
||||
const log = join(process.env.TMPDIR, `poe-208-${mutant}-${stage}.tap`);
|
||||
writeFileSync(log, (run.stdout ?? '') + (run.stderr ?? ''));
|
||||
const killedByAssertion = /ERR_ASSERTION/.test(run.stdout ?? '');
|
||||
const passed = run.status === expected && (stage !== 'kill' || killedByAssertion);
|
||||
results.push({ mutant, stage, exit: run.status, signal: run.signal, passed, log });
|
||||
console.log(JSON.stringify(results.at(-1)));
|
||||
if (!passed) {
|
||||
writeFileSync(join(process.env.TMPDIR, 'poe-208-mutants.json'), JSON.stringify(results, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
writeFileSync(join(process.env.TMPDIR, 'poe-208-mutants.json'), JSON.stringify(results, null, 2));
|
||||
@@ -1620,3 +1620,8 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-24T00:41Z | Duckworth (Opus 5.5) | row 323 round 1 static review on 6a0f0f4f8 (sage-3865, sage-3870) | APPROVE (static) C1 Sage full gate cli, manifest 903ef022 matches, fields sourced, tests read shipped bytes, tester mutant real, N1-N2 doc (duckworth-0300) |
|
||||
2026-10-24T01:13Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 Case1 project key | New goldie/row325 worktree at99930362e; pinned brief60d78e44e. Draft JSON key correction, new real-project-byte tests, sole old line70 key edit under mos-1564. Static checks only; requesting Sage CLEAR, no queue writes.
|
||||
2026-10-24T01:14Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 verified | sage-3882: new3/3, business264/264, one key mutant killed/restored, new3/3 again, per-run fresh shortTMP/load/helperCLEAR. Four manifest entries verify; sole old assertion key change, no queue writes. Candidate push/report to Sage and Gladstone next.
|
||||
2026-10-24 | Poe | Q246 / mos-1566, row 208 / 193d (#1706), poe/row208 | Started; approved design hash verified; implementation and isolated acceptance pending.
|
||||
2026-10-24 | Poe | Row 208 / 193d (#1706), poe/row208 | Builder acceptance green: webui 418/418, plugins 119/119, four UI mutants killed with bus bypass proofs; candidate prepared for Gladstone then Duckworth guard review.
|
||||
2026-10-24T04:29Z | Duckworth (Opus 5.5) | row 208 (193d) second review, guards only, on 4a4bef6c1 (sage-3934) | APPROVE (static), no bus change, every hidden control has a broker refusal, grant hint per-grant and self-only, N1 5 s poll (duckworth-0302) |
|
||||
2026-10-24T04:27Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 C2-09b rooms | Goldie owns build, Boyd reviews under sage-3915. New goldie/row265 worktree at ec86f2c84, claim rev3814; room verbs/parser/new tests prepared, no old tests changed. Sage-3932 slot: new parser14/14 and rooms18/18 passed; whole bus in progress before refusal mutants, stop rule active.
|
||||
2026-10-24T04:35Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 verified | First slot stopped at project-runtime import pin. mos-1567 narrowly authorizes pin and pure-source mount correction. sage-3942 rerun: sandbox10/10, parser15/15, rooms18/18, bus957/957, twelve refusal mutants killed/restored SHA71b32ee4, parser15/15 + rooms18/18 again. Slot returned goldie-0034; no further test runs. Boyd reviews RUNNER_FILES alongside rooms; own-branch push/claimant in-review next.
|
||||
@@ -207,6 +207,43 @@ install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha
|
||||
sudo -u mosaic -H git clone -q -b refactor /srv/mosaic/incoming/<sha>.bundle /srv/mosaic/src/mosaic-stack
|
||||
```
|
||||
|
||||
If the first install refuses, use Mos's retry path (mos-1565, ops-0291). Bootstrap can already have
|
||||
written `/srv/mosaic/.config/mosaic-dev/config.json` (`scripts/case2-install.sh:174`,
|
||||
`scripts/bootstrap.sh:13`, `scripts/mosaic-config.mjs:412-424`). The config dir can also hold
|
||||
`businesses/`, the business file, and library persona files and slot dirs
|
||||
(`scripts/case2-install.sh:200-211`, `scripts/case2-persona-import.mjs:300-308`). A validation refusal
|
||||
removes only the business file this run created; its directory remains (`scripts/case2-install.sh:217-219`).
|
||||
The copy may already exist under `/srv/mosaic/.local/share/mosaic-stack/copies/<sha>`
|
||||
(`scripts/case2-install.sh:80`, `scripts/stack-copy.sh:68`).
|
||||
|
||||
As root, move these three paths aside with a UTC suffix. Move, never delete, so their owner and mode
|
||||
stay with them (mos-1565). This is the default-instance retry for the paths in this sheet:
|
||||
|
||||
```sh
|
||||
(
|
||||
cd / || exit 1
|
||||
retry_stamp="$(date -u +%Y%m%dT%H%M%SZ)" || exit 1
|
||||
for retry_path in /root/mosaic-stack /srv/mosaic/src/mosaic-stack /srv/mosaic/.config/mosaic-dev; do
|
||||
if [ -e "$retry_path" ] || [ -L "$retry_path" ]; then
|
||||
retry_saved="$retry_path.$retry_stamp"
|
||||
if [ -e "$retry_saved" ] || [ -L "$retry_saved" ]; then
|
||||
echo "STOP: retry destination already exists: $retry_saved" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv -T -- "$retry_path" "$retry_saved" || exit 1
|
||||
fi
|
||||
done
|
||||
) || echo "STOP: could not move the first-install paths aside; do not continue"
|
||||
```
|
||||
|
||||
Then rerun the first-install block from its first line. Never use the upgrade fetch below on clones
|
||||
whose first install has not finished: it updates `refs/remotes/origin/refactor`, while copy requires
|
||||
the commit to be reachable from both that ref and `refs/heads/refactor` (`scripts/stack-copy.sh:44-48`; mos-1565).
|
||||
Recheck section 9 before starting units. If Jason already filled `claude-session.env`, the library
|
||||
proof file or `library-gitea`, have him re-place those files under the new config dir with section 9's
|
||||
owner and mode. Per-agent slots still use `mosaic credentials issue` or Approve, never a hand copy
|
||||
(section 9; mos-1565). The moved config dir preserves the earlier files.
|
||||
|
||||
After a first install, once both units are running (section 10), adopt the installed copy before
|
||||
any activate or upgrade. This step runs only on Mos's go, with Jason or Mos running it
|
||||
(`scripts/stack-release.sh:14`):
|
||||
@@ -254,6 +291,19 @@ When a run doesn't exit 0, send Mos the exit and the last lines of `/var/log/mos
|
||||
the old copy. The next run refuses `args-differ`, and a render from that file would put the old copy back.
|
||||
Move the file the result line names into place with `mv -f`. The two differ only in the `--copy` value.
|
||||
|
||||
To check a backup by hand before rollback or restore, run as the stack's user from its `bus/` directory:
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /bin/sh -c \
|
||||
'cd "<dataRoot>/backups/stack/<activation>/bus" && sha256sum -c ../SHA256SUMS'
|
||||
```
|
||||
|
||||
Expect `bus.sqlite: OK`; stop on a failed check. The set holds `bus/bus.sqlite`, and `SHA256SUMS` names
|
||||
`bus.sqlite` relative to `bus/` (`scripts/stack-release.sh:475-482`). Running `sha256sum -c SHA256SUMS`
|
||||
from the backup set's directory looks for `bus.sqlite` one directory too high (ops-0292). Restore and
|
||||
rehearsal use the same `bus/` form
|
||||
(`scripts/stack-release.sh:502`, `scripts/stack-release.sh:2244`).
|
||||
|
||||
After `case2-install.sh` succeeds, create the human CLI wrapper before running
|
||||
`stack-copy.sh check --units external`. Creating this wrapper is a separate live
|
||||
step requiring Mos's go (`scripts/stack-copy.sh:3`); the install does not create it. Run only on that go:
|
||||
@@ -275,11 +325,52 @@ it. The wrapper must name the same `<sha>` copy as the units.
|
||||
Precheck lines: none. Root's checkout at `<sha>` and the stack's user's clone: precheck line missing.
|
||||
After install, `stack-copy.sh check --units external` reads the copy the units run.
|
||||
|
||||
Before running `case2-install.sh`, check the exact file you will pass as `--business-file`. Each
|
||||
non-library `projects` key must equal the `id` in `<root>/.mosaic/project.json`, and that file must exist
|
||||
in the stack's user's clone. Library entries are skipped by validate (`packages/business/src/cli.mjs:72`).
|
||||
Run this read-only check as `mosaic` with section 2's pinned node:
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /opt/mosaic-stack/node-<v>/bin/node --input-type=module - '<business-file>' <<'MOSAIC_PROJECT_KEYS'
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
try {
|
||||
const business = JSON.parse(readFileSync(process.argv[2], 'utf8'));
|
||||
const projects = Object.entries(business.projects);
|
||||
if (!projects.length) throw new Error('business file declares no projects');
|
||||
for (const [id, entry] of projects) {
|
||||
if (entry.kind === 'library') continue;
|
||||
const file = join(entry.root, '.mosaic', 'project.json');
|
||||
const project = JSON.parse(readFileSync(file, 'utf8'));
|
||||
if (project.id !== id)
|
||||
throw new Error(`project file ${file} has id ${project.id}, but business ${business.id} declares it as ${id}`);
|
||||
console.log(`ok project ${id}: ${file} has id ${project.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`STOP: ${error.message}; do not install`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
MOSAIC_PROJECT_KEYS
|
||||
```
|
||||
|
||||
For .53 expect `ok project stack: /srv/mosaic/src/mosaic-stack/.mosaic/project.json has id stack`
|
||||
(`.mosaic/project.json:3`, `docs/guides/case1-business.mosaic-stack.json:10-11`). Any STOP or nonzero exit
|
||||
stops the install. Fix a mismatched business key, not the project id (mos-1563). The install otherwise
|
||||
reaches that refusal at step 7, after making the copy (`packages/business/src/cli.mjs:80`,
|
||||
`scripts/case2-install.sh:178`, `scripts/case2-install.sh:217`).
|
||||
|
||||
The stack's user's `scripts/case2-install.sh` (C2-13b, design 10 step 2b) makes the first copy with
|
||||
`stack-copy.sh copy <sha>`. The copy runs `npm ci` from `package-lock.json`, which installs the pinned
|
||||
harness, so the host needs to reach the npm registry during the install. The install runs no
|
||||
`release.sh package` or `activate`.
|
||||
|
||||
A missing `.mosaic/project.json` only warns: `project <id>: no project file at <path>; it has no project
|
||||
variables`. Validate reports that project `absent` and continues (`packages/business/src/cli.mjs:74-78`).
|
||||
An install can therefore pass step 7 without the project's variables. It captures validate's output
|
||||
and prints `business <id> validates` (`scripts/case2-install.sh:217-221`). Treat the missing-file warning as a stop,
|
||||
place the correct project file and repeat the check above before continuing (Duckworth 323 N2;
|
||||
`scripts/case2-install.sh:217-221`).
|
||||
|
||||
## 7. The args file
|
||||
|
||||
`/etc/mosaic-stack/dev.args` holds the `case2-unit.sh` arguments, one per line, with the serve flags after a
|
||||
@@ -318,7 +409,7 @@ file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's bus
|
||||
| Path | File | Mode | Owner | Profile | Precheck line |
|
||||
|---|---|---|---|---|---|
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `ZAI_API_KEY=...` | 0600 | mosaic | library | `<path> sets ZAI_API_KEY` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names | 0600 or 0400 | mosaic | library | none. `case2-unit.sh` refuses at render. Precheck line missing. |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names: one base64url line, at least 32 `[A-Za-z0-9_-]` characters, with one optional trailing LF (`packages/webui/src/serve.mjs:263-264`) | 0600 or 0400 | mosaic | library | none. Render checks metadata only and never opens the file (`scripts/case2-unit.sh:397-408`). Bad bytes refuse at Console start with exit 2 (`packages/webui/src/serve.mjs:267`, `packages/webui/src/cli.mjs:138-140`). Precheck line missing. |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | After a unit start, wait for loopback `/healthz` with the bounded retry in section 10 before `case2-precheck.sh --business <business> --library-status`: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ pane, persistent chat in the right, a draggable divider, document navigation, pi
|
||||
right-click thesaurus. It is not a reskinned row 154 editor. The first useful release, opened at W7 after the required gates, must
|
||||
pass writing, footnote, comment, tracked-change and page-layout save/reopen tests together.
|
||||
|
||||
D2. Q210/Q220 allow design and review now. O2 builds remain after the October 27 MVP work
|
||||
and never take its gate slot. S0 baseline clearance is conditional on M1–M14, Fenton/Filbert
|
||||
D2. Q210/Q220 allow design, review and builds ahead of the October 27 MVP work (mos-1571).
|
||||
O2 rows never take the gate slot ahead of a v1 row; they land only when the slot would
|
||||
otherwise sit idle. S0 baseline clearance is conditional on M1–M14, Fenton/Filbert
|
||||
at `origin/fenton/q195-s0` 74ec32013 (#1798, Sage PASS comment 30917). Q234 approves the
|
||||
exact measured frame CSP; the shell stays unchanged. mos-1363 requires W2 to remeasure zero
|
||||
violations with style-src 'self'; no rfd style hash is approved. These gates are satisfied
|
||||
@@ -585,14 +586,15 @@ Their implementation gates remain separate. No code row W1/W2/W6/W3a/W3b/W3/W4b/
|
||||
before S0 PASS (recorded for this baseline, with M1–M14 still binding). Each also observes its explicit dependencies, source ownership, the CSP gate
|
||||
and the MVP gate-slot priority. Q177 permits chaining reviewed candidates, not overlapping
|
||||
owners. Sage assigns model-only seats. Each implementation reviewer is Opus 5.5 and did not
|
||||
author the code. Fenton reviews this design and can review code only when not its author.
|
||||
author the code. A Codex-built row gets its first reviews from a separate Codex thread (Q246);
|
||||
the Opus 5.5 review runs once, on the landing candidate (mos-1571). Fenton reviews this design and can review code only when not its author.
|
||||
|
||||
Build order (mos-1470, Vikunja 571 comment 961): W7 first supplies reviewed conversion/format
|
||||
routing code with production import still closed, then W8 lands the Connections view, then
|
||||
W7's human opening step may open the first real chapter import. W8 is after W3/W4b and
|
||||
W7's closed routing candidate, before that W7 opening step. This is Mos's build-order rule,
|
||||
not a new REQ-LIB-8 condition or an addition to import acceptance. No converted real chapter
|
||||
exists without the view. O2 code remains after 10-27 and never ahead of MVP rows.
|
||||
exists without the view. O2 code may be built before 10-27 (mos-1571) but never lands ahead of MVP rows.
|
||||
|
||||
G1. Future code acceptance runs each touched test file independently and affected package
|
||||
suites on Sage's slot after MVP priority, plus required repository checks. Source builds need
|
||||
|
||||
@@ -59,6 +59,13 @@ fails it on any commit (row 323, mos-1561). A builder whose cli suite
|
||||
can't run says so in the row, and the cli suite counts from the lead's
|
||||
landing gate.
|
||||
|
||||
Evidence that lands with a row is plain text: logs, diffs, manifests. No
|
||||
gzip or other binary file, so the lead's push guard runs its binary and
|
||||
secret checks with no waiver. If a log is too big, cut it to the lines
|
||||
that prove the claim. Or keep it off refactor, in the seat's scratch, and
|
||||
cite its path and sha256 in BUILD.md (mos-1570, after row 265's 22 gzip
|
||||
logs).
|
||||
|
||||
### Handoffs
|
||||
|
||||
Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner
|
||||
|
||||
@@ -756,3 +756,15 @@ The guard is right. The test is the weak part: its prefix leaves no margin, and
|
||||
timeout. Fix: shorten the prefix and fail on a launch-failed log line instead of waiting 20 s. That's an
|
||||
old-test change, so it needs Mos's go (mos-1272). Bound to the next row that edits revoke-window.test.mjs.
|
||||
Until then, docs/plans/BRIEF-TEMPLATE.md's Gate section sets TMPDIR to `mktemp -d /mnt/storage/scratch/tmp/XXXXXX`.
|
||||
|
||||
## ops-0282 B2-2 closed by row 326
|
||||
|
||||
Sage. Row 326 (#1822) landed in f58049de0 (33f49f7d5). docs/guides/host-run-sheet.md now gives the Console
|
||||
proof file format in the .54 library table, so the B2-2 entry above is closed. Row226 N1-N4 stay open.
|
||||
|
||||
## Row 208 N1: user.js re-renders every 5 seconds
|
||||
|
||||
Sage, for Duckworth's N1 on row 208 (c31496, landed f58049de0). packages/webui/src/public/user.js:16
|
||||
re-renders the user view on a 5 s timer while the tab is visible, whether or not anything changed. That's a
|
||||
steady client and server load per open tab, not a correctness or guard issue. Fix: render on change, or poll the bus only for a change
|
||||
marker. Bound to the next row that edits user.js.
|
||||
+6
-5
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
<!-- mosaic-queue:begin -->
|
||||
|
||||
Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 3844 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -193,7 +193,7 @@ Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not e
|
||||
| 154 | A WYSIWYG Console editor built from reviewable source (Q168) | researcher; reviewers dewey, filbert | #1648 | done | 148 done | Mos's decision 123 go on the license and candidate before any code is imported, and later on the public/ diff; Dewey and Filbert approve naming the candidate manifest; webui, bus, cli, project suites and every scripts/test-*.sh green. Needs row 148 done. (sage) | `docs/plans/2026-10-16_q168-wysiwyg-editor.md` § A WYSIWYG Console editor built from reviewable source | — |
|
||||
| 155 | The human and the chief of staff configure the assistant (Q169) | darkwing; reviewers dewey, filbert | #1649 | done | 139 done, 141 done | Dewey and Filbert approve on #1649 naming the candidate manifest; Mos's decision 123 go on the public/ diff; bus, business, harness, cli, webui suites and every scripts/test-*.sh green; own window and tested migration if schema changes. Needs rows 139 and 141 done. (sage) | `docs/plans/2026-10-16_q169-assistant-config.md` § The human and the chief of staff configure the assistant | — |
|
||||
| 156 | The host starts the landing run once a merge decision is approved | dewey; reviewers filbert | #1651 | done | 125 done, 149 done | Filbert approves on #1651 naming the candidate manifest; Sage's full gate rerun at the landing target; the re-pin needs Mos's go (sage) | `docs/plans/2026-10-18_landing-run-launch.md` § The host starts the landing run once a merge decision is approved | — |
|
||||
| 157 | I2 install images, the install config key, the endpoint helper and the egress proxy | researcher; reviewers darkwing, filbert | #1652 | queued | 87 done, 163 done | Filbert and Darkwing approve on #1652 naming the candidate manifest, and Mos's provider-host output is on the issue; Sage's full gate green on the landing tree (sage) | `docs/plans/2026-10-18_containerized-install-rows.md` § I2 install images, the install config key, the endpoint helper and the egress proxy | Decision 96 orders I2-I6 after D1 and D2, which have no rows yet; the goal order sets priority |
|
||||
| 157 | I2 install images, the install config key, the endpoint helper and the egress proxy | bentina; reviewers boyd, darkwing | #1652 | in-review, round 2, request posted | 87 done, 163 done | Boyd (Codex, first review) and Darkwing approve on #1652 naming the candidate manifest, and Mos's provider-host output is on the issue; Sage's full gate green on the landing tree (sage) | `docs/plans/2026-10-18_containerized-install-rows.md` § I2 install images, the install config key, the endpoint helper and the egress proxy | Decision 96 orders I2-I6 after D1 and D2, which have no rows yet; the goal order sets priority |
|
||||
| 158 | I3 two broker listeners, the human-socket via value and the host relay | dewey; reviewers darkwing, filbert | #1653 | queued | 157 done | Filbert and Darkwing approve on #1653 naming the candidate manifest; Sage's full gate green on the landing tree (sage) | `docs/plans/2026-10-18_containerized-install-rows.md` § I3 two broker listeners, the human-socket via value and the host relay | Decision 96 orders I2-I6 after D1 and D2, which have no rows yet; the goal order sets priority |
|
||||
| 159 | I4 the Console in a container with signed-in human authority | darkwing; reviewers dewey, filbert | #1654 | queued | 158 done | Filbert and Dewey approve on #1654 naming the candidate manifest; Sage's full gate green on the landing tree; lands before M3 (sage) | `docs/plans/2026-10-18_containerized-install-rows.md` § I4 the Console in a container with signed-in human authority | Decision 96 orders I2-I6 after D1 and D2, which have no rows yet; the goal order sets priority. B1 is decision 147 |
|
||||
| 160 | I5 managed sessions as containers | researcher; reviewers dewey, filbert | #1655 | queued | 158 done | Filbert and Dewey approve on #1655 naming the candidate manifest; Sage's full gate green on the landing tree (sage) | `docs/plans/2026-10-18_containerized-install-rows.md` § I5 managed sessions as containers | Decision 96 orders I2-I6 after D1 and D2, which have no rows yet; the goal order sets priority |
|
||||
@@ -244,7 +244,7 @@ Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not e
|
||||
| 205 | 193b1a: the message filter (#1703; FED R3 and 193b1b build on it) | morgana; reviewers duckworth | #1703 | done | 196 done | Duckworth approves the manifest; bus suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b1a, the message filter | Landed ce6a96ad6 fast-forward on 0fa7bf421 (3 commits, 4 paths, manifest 858463b1). Duckworth APPROVE r1 c30375 (signed :124 test diff, mos-1262). Gate: Morgana slot205 10 files, bus 652/0, M1 M11-M14 killed, 0 skips; Sage test-*.sh 10/10, 3 by-design skips. N1/N3 to 205b or R3, N2 to 193b2 brief, N4 = #1782 before any user thread. Rebase next: 268 r4, 194, 236, 253, 274. |
|
||||
| 206 | 193b2: thread shares, schema 3k-3l after the #436b build (#1691; live needs a --migrate window with Jason) | launchpad; reviewers duckworth | #1704 | done | 205 done, 234 done, 236 done, 275 done | Duckworth approves the manifest; bus suite, cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b2, thread shares (schema 3l) | Landed 0cbe2e9b3 fast-forward on 17f415792 (10 commits, 22 paths), pushed 19:47Z. Duckworth APPROVE rev 3322 c30795 on 0cbe2e9b3; request rev 3320/3321 c30793 posted as jarvis. Gate (Launchpad, 9538e0fc8, tip differs in BUILD.md only): bus 850/0, cli 869/0, ten scripts/test-*.sh rc=0, touched files whole green, M2-M4 M7-M9 and Q2 killed. One placed fixture red (mos-1272). Schema 3l to 3m; live --migrate is Mos's go under Q173. |
|
||||
| 207 | 193c: mosaic access user verbs and --level viewer (#1691) | megavolt; reviewers duckworth | #1705 | done | 202 done | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193c, mosaic access user verbs and --level viewer | LANDED c4f77373f 22:49Z 10-22 (fast-forward from f0e3dcdf4). Rebuild of gated 9bb406c6c: range-diff 4/4 =, patch-ids equal, 4 manifest blobs equal. Duckworth APPROVE c30849 rev 3356. Gate on 712f79c0a under sage-3041: access.test 31/31, M1-M8 killed, cli 894/894, ten scripts green. N1 DEFERRED (next access.test row). No live change. |
|
||||
| 208 | 193d: Console shared list, share controls and viewer read-only (#1691; also after FED R6 when filed) | morgana; reviewers duckworth | #1706 | queued | 192 done, 206 done, 294 done | Duckworth approves the manifest; webui suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193d, the Console's shared list, share controls and viewer read-only | — |
|
||||
| 208 | 193d: Console shared list, share controls and viewer read-only (#1691; also after FED R6 when filed) | poe; reviewers duckworth, gladstone | #1706 | done | 192 done, 206 done, 294 done | Gladstone (Codex, first review) and Duckworth approve the manifest; webui suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193d, the Console's shared list, share controls and viewer read-only | LANDED f58049de0 (build 4a4bef6c1, Duckworth note f58049de0), combined with row 326 on one full gate 04:36-04:51Z: all package suites except queue 0 fail 0 skip (webui 418, plugins 119, cli 986), every scripts/test-*.sh green. Gladstone c31495, Duckworth c31496. #1706 closed c31504. N1 user.js 5 s re-render to DEFERRED. Bundle 9fc7754a. |
|
||||
| 209 | FED R8: the hub window on 10.1.1.54, a recorded acceptance (#414, r4.11 b93f098f0; live window run by Jason and Mos, after the Case 2 install #416, Bean and Chet live and both Hermes gateways stopped) | sage; reviewers duckworth | — | queued | — | Every entry check's output is in the R8 record with who ran it; Jason signs in to the hub and sends one message, Melanie revokes, his next request is refused; Duckworth checks the record (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R8, the hub window on 10.1.1.54 | Stays queued until its entry gates near: Case 2 install on .54 (#416, by 2026-10-27), Bean and Chet live, both Hermes gateways on .54 stopped, R7, #412 phase 2, #419 answer. Filed per mos-1073 after duckworth-0034 (Sage) |
|
||||
| 210 | Agent host access with sudo through the credential store: grant per agent and host, the stack does ssh and sudo, the agent never sees the key (#1707, Vikunja #424, Q183/Q184, due 10-27; chained on row 199's candidate) | gizmoduck; reviewers duckworth | #1707 | done | — | Duckworth approves DESIGN.md, then the manifest; bus, business, cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_host-credentials.md` § Brief: agent host access with sudo through the credential store (#424) | Landed f5753c0e8 on refactor (base 9d165be8c), pushed. Patch-id 723b1c83 matches git diff ad95c888c..0485384e9 on the 23 manifest paths; landed files pass r5 manifest 688d38b9. Duckworth approve r2 rev 2592, r3 29851. Mutants 52/53, M22b equivalent (rev 2584). Gate on ad95c888c+patch: packages 2796/0, scripts 429/0; landed blobs equal the gate tree. |
|
||||
| 211 | Force-push guard: scripts/git-hooks/pre-push refuses non-fast-forward and delete pushes from every worktree, and scripts/slot-busy.sh exits 1 on BUSY (#1709, Vikunja #426, mos-1078/1083) | darkwing; reviewers dewey | #1709 | in-progress | — | Dewey approves the manifest; the new or changed scripts/test-*.sh and every other scripts/test-*.sh green on Sage's gate; the mutants killed; hook installed in the canonical checkout with a scripted refused force-push (sage) | `docs/plans/2026-10-20_force-push-guard.md` § Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426) | — |
|
||||
@@ -301,7 +301,7 @@ Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not e
|
||||
| 262 | Email E06: IMAP source module (post-v1) | unassigned | #1767 | queued | 261 done | Reviewer approves the manifest; the row's named tests whole plus the mail suite and touched suites green on Sage's slot; reviewer-named mutants killed (post-v1, MVP keeps slot priority) (sage) | `docs/plans/2026-10-21_post-v1-row-briefs.md` § Brief: E06 IMAP source module | — |
|
||||
| 263 | Email E07: address and rule evaluator (post-v1) | unassigned | #1768 | queued | 258 done, 259 done, 260 done | Reviewer approves the manifest; the row's named tests whole plus the mail suite and touched suites green on Sage's slot; reviewer-named mutants killed (post-v1, MVP keeps slot priority) (sage) | `docs/plans/2026-10-21_post-v1-row-briefs.md` § Brief: E07 address and rule evaluator | — |
|
||||
| 264 | T1 item 7: independent sessions, compaction counts and clear context | gizmoduck; reviewers duckworth | #1769 | done | — | The named reviewer approves the manifest. The design's T1-T7 acceptance tests run whole plus every touched suite on Sage's slot; T3 waits on Vikunja 466 (N2, Claude's compaction count format and the managed dataRoot reader). The design's section 6 mutants are killed. (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | LANDED 84f53540d ff on e479521d3 14:18:23Z, 21 commits = gizmoduck/row264-r13 tip (no rewrite, range-diff identity). Manifest 257408e9 approved Duckworth rev 3214 (c30640). Gate r13 2cc34aef8: 20 files, bus 827, cli 837, webui 345/1 skip, 10 shells; M1-M4 assertion kills (mos-1279 B, M2 4 files). Overlap none (13, 257). N1 actor_run binds row 269; N2 esbuild skip later gate (sage-2695). |
|
||||
| 265 | Case 2 C2-09b rooms: room.open, room.send with @tags, room.read | rocko; reviewers dewey | #1770 | queued | 219 done | Dewey approves the manifest. The brief's tests run whole plus the bus suite and every touched suite on Sage's slot. Grep for hash pins on changed files before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09b rooms: room.open, room.send with @tags, room.read | — |
|
||||
| 265 | Case 2 C2-09b rooms: room.open, room.send with @tags, room.read | goldie; reviewers boyd | #1770 | done | 219 done | Boyd approves the manifest. The brief's tests run whole plus the bus suite and every touched suite on Sage's slot. Grep for hash pins on changed files before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09b rooms: room.open, room.send with @tags, room.read | LANDED 200dd396c (cherry-pick of ef4c93853, 50 blobs equal, log appends equal) 05:13Z on full gate 04:57-05:12Z: all package suites except queue 0 fail 0 skip (bus 957, cli 986, seat 218), every scripts/test-*.sh green. Boyd c31501 incl mos-1567 mount read. 22 gzip logs scanned decompressed, clean. #1770 closed c31510. Bundle 02c3c286. |
|
||||
| 266 | Case 2 C2-10c Console room view: merged timeline and @name completion | researcher; reviewers dewey | #1771 | queued | 225 done, 265 done | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every touched suite. Grep for hash pins on changed files before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10c Console room view: merged timeline and @name completion | Haiku high subagent under decision 129. |
|
||||
| 267 | Case 2 C2-15 hybrid universes: exclusions, pins and the end-to-end crossover | morgana; reviewers filbert | #1772 | queued | 216 done, 217 done, 224 done | Filbert approves the manifest. The brief's tests and the end-to-end crossover run whole plus every touched suite on Sage's slot. Grep for hash pins on changed files before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-15 hybrid universes: exclusions, pins and the end-to-end crossover | R8 accepted (sage-2218). Webby's R8 noise note (gosalyn-0127) is for the builder. |
|
||||
| 268 | Case 2 C2-01c playground levels: universe-level prefixes and draft universes | gizmoduck; reviewers webby | #1773 | done | — | Webby approves the manifest. The brief's tests run whole plus the library suite and every touched suite on Sage's slot; the brief's gate grep over packages and docs/case2 finds no library-playground-outside-book. Grep for hash pins on changed files before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01c playground levels: universe-level prefixes and draft universes | Landed c27207f6e fast-forward on ce6a96ad6 (1 commit, 9 paths, digest caa0fa4e). r4 is a carry of r3 aca53c4f0: range-diff =, patch-id 2942acc8 equal, blobs equal. Webby APPROVE round 2 on ecd9b35eb stands; carries webby-0112 (r3) and webby-0113 (r4, notes 34e1ab2bf). Gate: Gizmoduck r4 rerun brief 14/14, library 83/83, business 209/209, 0 skips; gate grep empty. 194 rebases next (business.test.mjs). |
|
||||
@@ -361,7 +361,8 @@ Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not e
|
||||
| 322 | F8/F9: case2-install.sh runs the library persona import from the stack copy, and its refusal carries the error line (433 Run B, ops-0273) | goldie; reviewers morgana | #1818 | required; done | — | Morgana approves (install path). The new case2-install test fails on 094124b4e and passes on the candidate; a mutant restoring REPO at :185 is killed; Sage's full landing gate runs every package suite and scripts/test-*.sh. Then ops-01 reruns Run B P1. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F8 and F9, the library persona import runs without the clone's node_modules | LANDED 467b05fe2, cherry-pick of 7b4ea4217 on 860afc2b6; added and removed lines identical, patch-id differs only by BUILD-LOG/SESSIONS append context (both sides kept). Pushed with tip 3569bf54f 23:32:54Z 10-23. Morgana APPROVE c31451 rev 3758, N1-N3 non-blocking. Sage full gate GREEN 23:32:28Z on 3569bf54f, all suites 0 fail, every scripts/test-*.sh rc 0. Next: ops-01 Run B P1 rerun on 467b05fe2 (mos-1553). Live unchanged 45dca69f6. |
|
||||
| 323 | Case 1 factory business file for .53: docs/guides/case1-business.mosaic-stack.json plus a loader and factory-check test (#414 stage 2, ops-0277) | goldie; reviewers duckworth | #1819 | done | — | Sage full gate: all package suites except queue plus every scripts/test-*.sh (mos-1455); Duckworth review; new test file and one bus.tester mutant on CLEAR (sage) | `docs/plans/2026-10-23_case1-business-file-brief.md` § Brief: the Case 1 factory business file | LANDED 571376c30 + evidence 6a0f0f4f8 (ff from dd6cb5872), tip 99930362e. Duckworth approve c31477 rev 3778 on 6a0f0f4f8. Sage gate green 00:54Z on 6a0f0f4f8: all package suites 0 fail, every scripts/test-*.sh rc 0. Root cause of the revoke-window red: TMPDIR length, tools.sock over 107 bytes (session.mjs:278). Brief template rule and DEFERRED entry in c25799928 (mos-1561). N1/N2 doc only. |
|
||||
| 324 | Tracker optional in the business file (Q247) | goldie; reviewers duckworth | #1820 | done | — | New tests only. Absent tracker: loader and validate pass, the bus boots with no credential-file error and no @sync. Present tracker unchanged. A mutant that skips the presence check is killed. Then whole touched test files, then the full landing gate. (sage) | `docs/plans/2026-10-23_optional-tracker-brief.md` § Brief: tracker becomes optional in the business file | LANDED 7e59ba8b3 + evidence dbdfcfd52 (ff from 3569bf54f), pushed with tip dd6cb5872 00:06:58Z 10-24. Duckworth APPROVE c31470 rev 3770 (static, N1 doc only). Goldie: new 5/5 and 4/4, business.test 21/21, presence mutant killed. Sage full gate GREEN 00:05:49Z on 64ea54083: every package suite 0 fail 0 skip (business 254, cli 986, bus 924, plugins 119, webui 411), every scripts/test-*.sh rc 0. Live unchanged 45dca69f6. Run B N1 for ops-01 P2/P3 (mos-1558). |
|
||||
| 325 | Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563) | goldie; reviewers gladstone | #1821 | in-review, round 1, request posted | — | New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455) (sage) | `docs/plans/2026-10-24_case1-project-key-brief.md` § Brief: re-key the Case 1 project to stack | — |
|
||||
| 325 | Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563) | goldie; reviewers gladstone | #1821 | done | — | New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455) (sage) | `docs/plans/2026-10-24_case1-project-key-brief.md` § Brief: re-key the Case 1 project to stack | LANDED c5f96836d (ff from 99930362e), tip 3d2569ce2. Gladstone approve c31484 rev 3788 on c5f96836d. Sage gate green 01:31Z on c5f96836d: all package suites 0 fail (business 264, cli 986), every scripts/test-*.sh rc 0. Project key is now stack, matching .mosaic/project.json; new case1-project-key test checks the real project file. Old-test line :70 under mos-1564. |
|
||||
| 326 | Host run sheet: five operator gaps (B2-2 proof format, 323 N2, pre-install key check, failed-first-install retry, backup SHA256SUMS form) | goldie; reviewers gladstone | #1822 | done | — | Gladstone approves the manifest; every new claim cites its line; no test reads the sheet; Sage's full landing gate green (sage) | `docs/plans/2026-10-24_run-sheet-doc-brief.md` § Brief: five run-sheet gaps | LANDED 33f49f7d5 (cherry-pick of a1b13c60e, patch-id match, sheet e4c14516) in f58049de0 with row 208 on one full gate 04:36-04:51Z, all suites green. Gladstone c31489. #1822 closed c31507. B2-2 closes in DEFERRED. Bundle 9fc7754a. |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
|
||||
+1707
-33
File diff suppressed because it is too large.
Load diff
@@ -242,6 +242,9 @@ a receipt with empty `changes`. A refusal is `action.refused` with its code alon
|
||||
| `replies` | `{id}`: a message UUID; human and reader sessions only. The messages whose `in_reply_to` is that id, oldest first, at most 50. Writes nothing. |
|
||||
| `project.create` | `{name,repo?}`; human sessions only (row 121, #1615). `repo` must name one of the business's `projects` (`unknown-repo`); a second project with the same name refuses `project-exists`. Returns `{id}`. |
|
||||
| `project.list` | No args; human sessions only. The business's projects, oldest first. |
|
||||
| `room.open` | `{project}`; human only. Creates/reuses the caller's canonical member threads, named by sorted `THREAD_DEFINITIONS` instance keys. A taken name/owner/instance refuses `room-thread-name-taken` atomically. Returns `{project,threads:[{id,instance,name}]}`. |
|
||||
| `room.send` | `{project,body}`; human only, room must be fully open (`room-not-open`). Leading whitespace and `@name` tokens (including a final EOF tag) address current display names or member keys; no tags broadcasts, repeats send once. Unknown/ambiguous members refuse `unknown-addressee`/`addressee-ambiguous`; tags-only refuses `invalid-request`. Stores the original body, one normal message/route/human.input per target in one transaction, with one shared message timestamp. Returns `{ids,to}` in member-key order. |
|
||||
| `room.read` | `{project,after?,limit?}`; human only, complete owned room. Oldest first across all sessions of member threads. Returns entries with first-message fields plus `thread`, `session`, `instance`, `ids` and `to`. Only consecutive human rows with equal at/body/class and distinct instances merge; repeated instances split identical sends. `limit` counts merged entries (default 50, 1–200). Continue after the last entry's last `ids` element; an `after` outside the room refuses `message-not-found`. No tester access or schema change. |
|
||||
| `thread.create` | `{project,name}`; human sessions only. The thread belongs to the caller and to the business's one thread instance (Projects and threads, below). Returns `{id,instance}`. Refuses `project-not-found`, `thread-exists`, `thread-instance-missing` or `thread-instance-ambiguous`. |
|
||||
| `thread.list` | `{project?,shared?}`; human sessions only. The caller's own threads, oldest first. With `shared: true` (row 206), the threads others own that the caller may read (Thread shares, below), each row adding `owner`. |
|
||||
| `thread.open` | `{thread,session?}`; human sessions only. Records a `thread_opens` row, which asks the launcher to start the thread's session. Returns `{opened}`. |
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// C2-09b: a pure leading-address parser, shared with the room UI's parity test.
|
||||
// The remainder is for empty-body checks; the broker always stores the original input.
|
||||
export function parseAddress(input) {
|
||||
if (typeof input !== 'string') throw new TypeError('body must be a string');
|
||||
const tags = [];
|
||||
let body = input.trimStart();
|
||||
while (true) {
|
||||
const token = /^@(\S+)(?:\s+|$)/u.exec(body);
|
||||
if (!token) break;
|
||||
tags.push(token[1]);
|
||||
body = body.slice(token[0].length);
|
||||
}
|
||||
return { tags, body };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
||||
import { parseAddress } from './address.mjs';
|
||||
import { isAbsolute, dirname } from 'node:path';
|
||||
import { initializeControl, processingStatus } from '../../processing-control/src/state.mjs';
|
||||
import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';
|
||||
@@ -6418,6 +6419,99 @@ export class Broker {
|
||||
.all('SELECT id,at,name,repo FROM projects WHERE business=? ORDER BY seq', s.business)
|
||||
.filter((p) => !s.user || this.#granted(s, 'project.list', p.id, null));
|
||||
}
|
||||
// C2-09b: canonical per-instance threads form one human-owned project room.
|
||||
case 'room.open':
|
||||
case 'room.send':
|
||||
case 'room.read': {
|
||||
this.#human(s);
|
||||
const opening = verb === 'room.open';
|
||||
const sending = verb === 'room.send';
|
||||
keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'],
|
||||
sending ? ['project', 'body'] : ['project']);
|
||||
const project = identifier(a.project);
|
||||
if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');
|
||||
const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort();
|
||||
if (!members.length) fail('thread-instance-missing');
|
||||
const owner = this.#owner(s);
|
||||
// Preflight all collisions/missing threads before open creates or send writes anything.
|
||||
const threads = members.map((instance) => {
|
||||
const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance);
|
||||
if (t && (t.instance !== instance || t.owner !== owner))
|
||||
fail(opening ? 'room-thread-name-taken' : 'room-not-open');
|
||||
if (!t && !opening) fail('room-not-open');
|
||||
return t ?? { instance, name: instance };
|
||||
});
|
||||
if (opening) {
|
||||
return { project, threads: threads.map((t) => ({
|
||||
id: t.id ?? this.#dispatch(s, 'thread.create', { project, name: t.name, instance: t.instance }).id,
|
||||
instance: t.instance, name: t.name,
|
||||
})) };
|
||||
}
|
||||
if (sending) {
|
||||
string(a.body, 32768);
|
||||
const addressed = parseAddress(a.body);
|
||||
if (!addressed.body.trim()) fail('invalid-request');
|
||||
const selected = new Set();
|
||||
for (const tag of addressed.tags) {
|
||||
const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance)));
|
||||
if (!candidates.size) fail('unknown-addressee');
|
||||
if (candidates.size > 1) fail('addressee-ambiguous');
|
||||
selected.add([...candidates][0]);
|
||||
}
|
||||
const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance));
|
||||
const sessions = targets.map((t) => {
|
||||
const session = this.#defaultSession(t.id);
|
||||
this.#sessionFence(session);
|
||||
this.#checkMessaging(s.business, 'human', t.instance);
|
||||
return session;
|
||||
});
|
||||
const at = this.#now();
|
||||
const ids = targets.map((t, i) => {
|
||||
const mid = randomUUID();
|
||||
this.#store.run(
|
||||
'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
||||
mid, at, s.business, 'human', 'human-cli', t.instance, 'INFO', null, null, null, a.body,
|
||||
);
|
||||
this.#route(s.business, mid, t.instance, sessions[i]);
|
||||
this.#event(s, 'human.input', { kind: 'instruction', message: mid }, mid);
|
||||
return mid;
|
||||
});
|
||||
return { ids, to: targets.map((t) => t.instance) };
|
||||
}
|
||||
const limit = a.limit ?? THREAD_PAGE;
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_PAGE_MAX) fail('invalid-request');
|
||||
const tids = threads.map((t) => t.id);
|
||||
const placeholders = tids.map(() => '?').join(',');
|
||||
let after = 0;
|
||||
if (a.after !== undefined) {
|
||||
after = this.#store.get(
|
||||
`SELECT m.seq FROM messages m JOIN message_threads mt ON mt.message=m.id WHERE m.id=? AND m.business=? AND mt.thread IN (${placeholders})`,
|
||||
identifier(a.after), s.business, ...tids,
|
||||
)?.seq;
|
||||
if (after === undefined) fail('message-not-found');
|
||||
}
|
||||
// At most one row per instance joins a group; one lookahead row completes the last page.
|
||||
const rows = this.#store.all(
|
||||
`SELECT m.*,mt.thread,mt.session,t.instance FROM messages m JOIN message_threads mt ON mt.message=m.id JOIN threads t ON t.id=mt.thread
|
||||
WHERE m.business=? AND mt.thread IN (${placeholders}) AND m.seq>? ORDER BY m.seq LIMIT ${limit * members.length + 1}`,
|
||||
s.business, ...tids, after,
|
||||
);
|
||||
const entries = [];
|
||||
let previous;
|
||||
for (const m of rows) {
|
||||
const entry = entries.at(-1);
|
||||
if (entry && m.from_role === 'human' && previous.from_role === 'human' && m.seq === previous.seq + 1
|
||||
&& m.at === previous.at && m.body === previous.body && m.class === previous.class && !entry.to.includes(m.instance)) {
|
||||
entry.ids.push(m.id);
|
||||
entry.to.push(m.instance);
|
||||
} else {
|
||||
if (entries.length === limit) break;
|
||||
entries.push({ ...m, ids: [m.id], to: [m.instance] });
|
||||
}
|
||||
previous = m;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
// A thread belongs to the user who made it and to one declared thread instance.
|
||||
// Row 177 (#1675): a tester's thread is owned by `tester`, so neither reaches the other's.
|
||||
case 'thread.create': {
|
||||
|
||||
@@ -3,3 +3,4 @@ export { startBroker } from './runtime.mjs';
|
||||
export { Client } from './client.mjs';
|
||||
export { views } from './views.mjs';
|
||||
export { busBusiness } from './business.mjs';
|
||||
export { parseAddress } from './address.mjs';
|
||||
@@ -0,0 +1,16 @@
|
||||
// C2-09b / C2-10c: reusable parser vectors. Keep the expected result independent of the parser.
|
||||
export const addressCases = [
|
||||
['one', '@continuity x', { tags: ['continuity'], body: 'x' }],
|
||||
['many', '@bean @chet hello', { tags: ['bean', 'chet'], body: 'hello' }],
|
||||
['repeat', '@bean @bean hi', { tags: ['bean', 'bean'], body: 'hi' }],
|
||||
['plain', 'hello @bean', { tags: [], body: 'hello @bean' }],
|
||||
['stop', '@bean hello @chet', { tags: ['bean'], body: 'hello @chet' }],
|
||||
['EOF tag', '@continuity', { tags: ['continuity'], body: '' }],
|
||||
['EOF last tag', '@bean @chet', { tags: ['bean', 'chet'], body: '' }],
|
||||
['leading whitespace', ' \t\n@bean\t@chet\nhello ', { tags: ['bean', 'chet'], body: 'hello ' }],
|
||||
['tags only', '@bean \t\n', { tags: ['bean'], body: '' }],
|
||||
['empty', '', { tags: [], body: '' }],
|
||||
['whitespace', ' \t', { tags: [], body: '' }],
|
||||
['bare at', '@ hello', { tags: [], body: '@ hello' }],
|
||||
['punctuation is a name', '@unknown, hi', { tags: ['unknown,'], body: 'hi' }],
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { parseAddress } from '../src/address.mjs';
|
||||
import { addressCases } from './address-cases.mjs';
|
||||
|
||||
for (const [label, input, expected] of addressCases) {
|
||||
test(`parseAddress: ${label}`, () => assert.deepEqual(parseAddress(input), expected));
|
||||
}
|
||||
test('parseAddress rejects non-text without coercion', () => {
|
||||
for (const input of [null, undefined, {}, 1]) assert.throws(() => parseAddress(input), TypeError);
|
||||
});
|
||||
|
||||
// mos-1567: new coverage, separate from the pre-existing broker import pin.
|
||||
test('the mounted address parser has no import dependency', () => {
|
||||
assert.doesNotMatch(readFileSync(new URL('../src/address.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
||||
});
|
||||
@@ -14,6 +14,8 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n
|
||||
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [
|
||||
// Row 210 (#1707): createHash gives host.run events the argv, stdin and output sha256 values.
|
||||
"import { randomUUID, randomBytes, createHash } from 'node:crypto';",
|
||||
// Row 265 (#1770): pure room address parsing, mounted in RUNNER_FILES beside the broker.
|
||||
"import { parseAddress } from './address.mjs';",
|
||||
// Row 65 (#1558): bindLaunch checks a run's transcript path is absolute.
|
||||
"import { isAbsolute, dirname } from 'node:path';",
|
||||
// Row 191 / sage-1367: the one additional read-only CODE source, with no project/git imports.
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker, THREAD_PAGE, THREAD_PAGE_MAX, TESTER_VERBS } from '../src/broker.mjs';
|
||||
|
||||
const MEMBERS = ['bean', 'chet', 'continuity'];
|
||||
const shipped = (name) => JSON.parse(readFileSync(new URL(`../../../roles/${name}.json`, import.meta.url), 'utf8'));
|
||||
function setup(t, { deny = false, empty = false } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-rooms-'));
|
||||
const store = new Store(root);
|
||||
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
||||
const role = (definition) => ({ definition, authority: { withinRole: ['message.send'], crossRole: [] } });
|
||||
// Deliberately unsorted, with two instances of one definition and a non-member chief.
|
||||
const roles = { continuity: role('continuity'), chet: role('companion'), bean: role('companion'), chief: role('chief-of-staff') };
|
||||
const definitions = {
|
||||
'chief-of-staff': {}, companion: { messaging: shipped('companion').messaging },
|
||||
continuity: { messaging: deny ? { sendTo: ['human'], receiveFrom: [] } : shipped('continuity').messaging },
|
||||
};
|
||||
const b = new Broker({ store, businesses: { demo: {
|
||||
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
|
||||
roles: empty ? { chief: roles.chief } : roles, definitions,
|
||||
projects: { stack: { root: '/nonexistent' } }, vars: { 'bus.tester': true },
|
||||
} } });
|
||||
const caps = {
|
||||
human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }),
|
||||
tester: b.bindTester({ business: 'demo' }),
|
||||
};
|
||||
const call = (verb, args = {}, who = 'human') => b.request(caps[who], { verb, args });
|
||||
caps.chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
|
||||
call('role.claim', {}, 'chief');
|
||||
const project = call('project.create', { name: 'room', repo: 'stack' }).id;
|
||||
const open = () => call('room.open', { project });
|
||||
const send = (body, extra = {}) => call('room.send', { project, body, ...extra });
|
||||
const read = (extra = {}) => call('room.read', { project, ...extra });
|
||||
const effects = () => ['messages', 'message_threads', 'deliveries'].map((table) => store.get(`SELECT count(*) n FROM ${table}`).n)
|
||||
.concat(store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n);
|
||||
const refuse = (code, fn) => {
|
||||
const before = effects();
|
||||
assert.throws(fn, { code });
|
||||
assert.deepEqual(effects(), before, `${code}: no message, route, delivery or human.input`);
|
||||
};
|
||||
return { b, caps, store, call, project, open, send, read, effects, refuse };
|
||||
}
|
||||
|
||||
test('room.open sorts all threaded instances, creates defaults, and reuses caller-owned threads', (t) => {
|
||||
const { open, call, project, store } = setup(t);
|
||||
const existing = call('thread.create', { project, name: 'chet', instance: 'chet' }).id;
|
||||
const result = open();
|
||||
assert.equal(result.project, project);
|
||||
assert.deepEqual(result.threads.map(({ instance, name }) => [instance, name]), MEMBERS.map((x) => [x, x]));
|
||||
assert.equal(result.threads[1].id, existing);
|
||||
for (const thread of result.threads) {
|
||||
assert.equal(store.get('SELECT owner FROM threads WHERE id=?', thread.id).owner, 'jason');
|
||||
assert.equal(call('session.list', { thread: thread.id }).filter((s) => s.default).length, 1);
|
||||
}
|
||||
assert.deepEqual(open(), result);
|
||||
assert.equal(store.get('SELECT count(*) n FROM threads').n, 3);
|
||||
});
|
||||
|
||||
for (const collision of ['instance', 'owner']) {
|
||||
test(`room.open preflights ${collision} collisions atomically`, (t) => {
|
||||
const { call, project, open, store } = setup(t);
|
||||
call('thread.create', { project, name: 'continuity', instance: collision === 'instance' ? 'bean' : 'continuity' },
|
||||
collision === 'owner' ? 'tester' : 'human');
|
||||
assert.throws(open, { code: 'room-thread-name-taken' });
|
||||
assert.deepEqual(store.all('SELECT name FROM threads').map((r) => r.name), ['continuity']);
|
||||
assert.equal(store.get('SELECT count(*) n FROM sessions').n, 1);
|
||||
});
|
||||
}
|
||||
|
||||
test('room verbs require the human, and none is a tester verb', (t) => {
|
||||
const { call, project, open, refuse } = setup(t);
|
||||
open();
|
||||
for (const verb of ['room.open', 'room.send', 'room.read']) {
|
||||
const args = { project, ...(verb === 'room.send' ? { body: 'hello' } : {}) };
|
||||
assert.ok(!TESTER_VERBS.includes(verb));
|
||||
refuse('tester-refused', () => call(verb, args, 'tester'));
|
||||
refuse('human-required', () => call(verb, args, 'chief'));
|
||||
}
|
||||
});
|
||||
|
||||
test('room verbs refuse missing projects and zero member businesses', (t) => {
|
||||
const { call, refuse } = setup(t);
|
||||
for (const verb of ['room.open', 'room.send', 'room.read']) {
|
||||
refuse('project-not-found', () => call(verb, { project: randomUUID(), ...(verb === 'room.send' ? { body: 'hello' } : {}) }));
|
||||
}
|
||||
const empty = setup(t, { empty: true });
|
||||
assert.throws(empty.open, { code: 'thread-instance-missing' });
|
||||
});
|
||||
|
||||
for (const state of ['missing', 'foreign-owner', 'wrong-instance']) {
|
||||
test(`send/read refuse a ${state} canonical member without effects`, (t) => {
|
||||
const { call, project, send, read, refuse } = setup(t);
|
||||
for (const instance of MEMBERS) {
|
||||
if (instance === 'continuity' && state === 'missing') continue;
|
||||
call('thread.create', { project, name: instance, instance: instance === 'continuity' && state === 'wrong-instance' ? 'bean' : instance },
|
||||
instance === 'continuity' && state === 'foreign-owner' ? 'tester' : 'human');
|
||||
}
|
||||
refuse('room-not-open', () => send('hello'));
|
||||
refuse('room-not-open', read);
|
||||
});
|
||||
}
|
||||
|
||||
test('room.send targets a key, broadcasts without tags, and preserves original text and ordinary route/input rows', (t) => {
|
||||
const { open, send, store, read } = setup(t);
|
||||
const threads = open().threads;
|
||||
const one = send('@continuity x');
|
||||
assert.deepEqual(one.to, ['continuity']);
|
||||
assert.equal(one.ids.length, 1);
|
||||
assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', one.ids[0]).thread, threads[2].id);
|
||||
const body = ' \t@bean @chet hello @continuity';
|
||||
const two = send(body);
|
||||
assert.deepEqual(two.to, ['bean', 'chet']);
|
||||
const all = send('hello @bean');
|
||||
assert.deepEqual(all.to, MEMBERS);
|
||||
const rows = store.all('SELECT * FROM messages ORDER BY seq');
|
||||
assert.deepEqual(rows.map((r) => r.body), ['@continuity x', body, body, ...MEMBERS.map(() => 'hello @bean')]);
|
||||
assert.equal(new Set(rows.slice(-3).map((r) => r.at)).size, 1);
|
||||
assert.ok(rows.every((r) => r.from_role === 'human' && r.from_run === 'human-cli' && r.class === 'INFO'));
|
||||
assert.equal(store.get('SELECT count(*) n FROM message_threads').n, 6);
|
||||
assert.equal(store.get("SELECT count(*) n FROM deliveries WHERE op='routed'").n, 6);
|
||||
assert.equal(store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n, 6);
|
||||
assert.deepEqual(read().map((r) => r.to), [['continuity'], ['bean', 'chet'], MEMBERS]);
|
||||
});
|
||||
|
||||
test('current display names and repeated tags resolve once; a replaced display name is unknown', (t) => {
|
||||
const { open, call, send, refuse } = setup(t);
|
||||
open();
|
||||
call('name.set', { instance: 'bean', name: 'beanie' });
|
||||
assert.deepEqual(send('@beanie @bean @beanie hi').to, ['bean']);
|
||||
call('name.set', { instance: 'bean', name: 'newbeanie' });
|
||||
refuse('unknown-addressee', () => send('@beanie hi'));
|
||||
assert.deepEqual(send('@newbeanie hi').to, ['bean']);
|
||||
});
|
||||
|
||||
test('a legacy name colliding with another member key is ambiguous; its own key resolves once', (t) => {
|
||||
const { open, send, store, refuse } = setup(t);
|
||||
open();
|
||||
// Naming policy now forbids key collisions; legacy data can still contain them. Append real rows.
|
||||
store.run('INSERT INTO instance_names(at,business,instance,name,by) VALUES(?,?,?,?,?)', new Date().toISOString(), 'demo', 'bean', 'chet', 'fixture');
|
||||
refuse('addressee-ambiguous', () => send('@chet hi'));
|
||||
store.run('INSERT INTO instance_names(at,business,instance,name,by) VALUES(?,?,?,?,?)', new Date().toISOString(), 'demo', 'bean', 'bean', 'fixture');
|
||||
assert.deepEqual(send('@bean @bean hi').to, ['bean']);
|
||||
});
|
||||
|
||||
test('unknown or non-member addressees and tags-only bodies leave no message effects', (t) => {
|
||||
const { open, send, refuse } = setup(t);
|
||||
open();
|
||||
for (const body of ['@nobody x', '@chief x', '@bean @nobody x']) refuse('unknown-addressee', () => send(body));
|
||||
for (const body of ['@continuity', ' \t@bean\n@chet ', '', ' \t']) refuse('invalid-request', () => send(body));
|
||||
});
|
||||
|
||||
test('messaging denial on the last target rolls back the entire broadcast; allowed subset still works', (t) => {
|
||||
const { open, send, refuse } = setup(t, { deny: true });
|
||||
open();
|
||||
refuse('message-sender-refused', () => send('all'));
|
||||
assert.deepEqual(send('@bean @chet okay').to, ['bean', 'chet']);
|
||||
});
|
||||
|
||||
test('a clearing last member fences the whole send; another active target still accepts', (t) => {
|
||||
const { b, caps, open, call, send, refuse } = setup(t);
|
||||
const thread = open().threads[2].id;
|
||||
caps.worker = b.bindLaunch({ business: 'demo', role: 'continuity', run: 'continuity-run', harness: 'pi', thread });
|
||||
call('role.claim', {}, 'worker');
|
||||
const session = call('session.list', { thread })[0].id;
|
||||
call('session.clear', { thread, session, revision: 0, op: 'room-clear', confirm: true });
|
||||
refuse('session-clearing', () => send('all'));
|
||||
assert.deepEqual(send('@bean okay').to, ['bean']);
|
||||
});
|
||||
|
||||
test('room.read groups one send, pages by the last grouped id, and rejects foreign or absent cursors', (t) => {
|
||||
const { open, send, read, call, project, refuse } = setup(t);
|
||||
const threads = open().threads;
|
||||
const first = send('first');
|
||||
const second = send('@continuity second');
|
||||
const page = read({ limit: 1 });
|
||||
assert.equal(page.length, 1);
|
||||
assert.deepEqual(page[0].ids, first.ids);
|
||||
assert.deepEqual(page[0].to, MEMBERS);
|
||||
assert.equal(page[0].thread, threads[0].id);
|
||||
assert.equal(page[0].instance, 'bean');
|
||||
assert.deepEqual(read({ after: first.ids.at(-1), limit: 1 }).map((r) => r.ids), [second.ids]);
|
||||
assert.deepEqual(read({ after: second.ids[0] }), []);
|
||||
const other = call('project.create', { name: 'other', repo: 'stack' }).id;
|
||||
const thread = call('thread.create', { project: other, name: 'bean', instance: 'bean' }).id;
|
||||
const foreign = call('thread.send', { thread, body: 'elsewhere' }).id;
|
||||
const noncanonical = call('thread.create', { project, name: 'side', instance: 'bean' }).id;
|
||||
const side = call('thread.send', { thread: noncanonical, body: 'side' }).id;
|
||||
for (const after of [foreign, side, randomUUID()]) refuse('message-not-found', () => read({ after }));
|
||||
for (const limit of [0, -1, 1.1, THREAD_PAGE_MAX + 1, '1']) refuse('invalid-request', () => read({ limit }));
|
||||
});
|
||||
|
||||
test('room.read keeps identical sends at the same timestamp separate and includes every session', (t) => {
|
||||
const { open, call, project, store, read } = setup(t);
|
||||
const threads = open().threads;
|
||||
const at = '2026-10-24T00:00:00.000Z';
|
||||
const batches = [[], []];
|
||||
// Broker clocks are monotonic. Append two independently valid transaction-shaped batches to
|
||||
// exercise the historical same-at boundary without altering production clocks or existing rows.
|
||||
for (const batch of batches) for (const thread of threads) {
|
||||
const id = randomUUID();
|
||||
batch.push(id);
|
||||
const session = call('session.list', { thread: thread.id })[0].id;
|
||||
store.run('INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,body) VALUES(?,?,?,?,?,?,?,?)',
|
||||
id, at, 'demo', 'human', 'human-cli', thread.instance, 'INFO', 'same');
|
||||
store.run('INSERT INTO message_threads(message,thread,session) VALUES(?,?,?)', id, thread.id, session);
|
||||
}
|
||||
assert.deepEqual(read().map((r) => r.ids), batches);
|
||||
assert.deepEqual(read({ limit: 1 })[0].ids, batches[0]);
|
||||
assert.deepEqual(read({ after: batches[0].at(-1) }).map((r) => r.ids), [batches[1]]);
|
||||
const created = call('session.create', { thread: threads[0].id, name: 'later', op: 'new-later' });
|
||||
call('thread.send', { thread: threads[0].id, session: created.id, body: 'later session' });
|
||||
assert.equal(read().at(-1).body, 'later session');
|
||||
assert.equal(read().at(-1).session, created.id);
|
||||
assert.equal(read().at(-1).thread, threads[0].id);
|
||||
assert.equal(store.get('SELECT count(*) n FROM threads WHERE project=?', project).n, 3);
|
||||
});
|
||||
|
||||
test('room.read defaults and caps the number of merged entries, not recipient rows', (t) => {
|
||||
const { open, send, read } = setup(t);
|
||||
open();
|
||||
for (let i = 0; i <= THREAD_PAGE_MAX; i++) send(`message ${i}`);
|
||||
assert.equal(read().length, THREAD_PAGE);
|
||||
assert.equal(read({ limit: THREAD_PAGE_MAX }).length, THREAD_PAGE_MAX);
|
||||
assert.ok(read({ limit: 1 })[0].to.length === 3);
|
||||
});
|
||||
|
||||
test('room.read includes an agent reply with its thread and instance without merging it with human rows', (t) => {
|
||||
const { b, caps, open, call, send, read, refuse, project } = setup(t);
|
||||
const thread = open().threads[0].id;
|
||||
send('question');
|
||||
caps.worker = b.bindLaunch({ business: 'demo', role: 'bean', run: 'bean-run', harness: 'pi', thread });
|
||||
call('role.claim', {}, 'worker');
|
||||
const reply = call('message.send', { to: 'human', body: 'answer' }, 'worker');
|
||||
const entries = read();
|
||||
assert.equal(entries.length, 2);
|
||||
assert.deepEqual(entries[0].to, MEMBERS);
|
||||
assert.equal(entries[1].id, reply.id);
|
||||
assert.equal(entries[1].from_role, 'bean');
|
||||
assert.equal(entries[1].thread, thread);
|
||||
assert.equal(entries[1].instance, 'bean');
|
||||
refuse('human-required', () => call('room.read', { project }, 'worker'));
|
||||
});
|
||||
@@ -9,6 +9,9 @@ const VERBS = new Set([
|
||||
"thread.messages",
|
||||
"thread.send",
|
||||
"thread.open",
|
||||
"thread.shares",
|
||||
"thread.share",
|
||||
"thread.unshare",
|
||||
"session.list",
|
||||
"session.create",
|
||||
"session.clear",
|
||||
|
||||
@@ -57,6 +57,7 @@ export const RUNNER_FILES = Object.freeze([
|
||||
"packages/harness/src/pi-extension.mjs",
|
||||
"packages/bus/src/client.mjs",
|
||||
"packages/bus/src/broker.mjs",
|
||||
"packages/bus/src/address.mjs",
|
||||
"packages/bus/src/landing.mjs",
|
||||
"packages/business/src/vocabulary.mjs",
|
||||
"packages/plugins/src/kinds.mjs",
|
||||
|
||||
@@ -1091,3 +1091,16 @@ During Q175's read-only interim both buttons show as unavailable and point to
|
||||
`mosaic halt` / `mosaic resume` in the instance CLI. There is no halt-only send
|
||||
exception or alternate authentication path. Landing the UI and activating it
|
||||
remain separately gated.
|
||||
|
||||
Row 208 (193d): Chat includes a **Shared with me** list. Shared threads are
|
||||
read-only, including for the business owner. On your own writable threads,
|
||||
**Share read-only** accepts `user:ID`, the business owner's name, or `*` for
|
||||
everyone with coverage; **Unshare** ends that target's share. A share grants
|
||||
no project or instance access of its own.
|
||||
|
||||
Viewers can read shared threads and their old own threads. Their pages omit
|
||||
creation, composer, resume, share and session mutation controls. Mixed grants
|
||||
apply separately to each project and instance; a collaborator grant elsewhere
|
||||
does not enable writes here. Grant hints refresh on each user chat render and
|
||||
background refresh, while the bus decides every operation from live grants.
|
||||
The existing user notice names the governance records visible to the owner.
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"sourceManifestSha256": "e5a799a6693321be71c4e99080b9f89ba12a304c3092d05839bb85355194fe76",
|
||||
"archive": {
|
||||
"bytes": 10775600,
|
||||
"sha256": "cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59"
|
||||
},
|
||||
"counts": {
|
||||
"synsets": 117659,
|
||||
"headwords": 147306,
|
||||
"literalLemmas": 149229
|
||||
},
|
||||
"countMethod": "Union of first-field headwords across four index.* files; cross-checked with lowercased data.* lemmas stripping terminal (a)/(p)/(ip). Literal data lemmas counted without normalization.",
|
||||
"openNote": "Princeton statistics page union 147278 differs by 28; unresolved, accepted in sage-3972.",
|
||||
"tools": {
|
||||
"node": "v26.8.1",
|
||||
"initialImportPython": "3.12.8",
|
||||
"converter": "Node.js >=24, standard library only; JSON UTF-8 LF, no timestamps, no locale sorting",
|
||||
"importer": "Node.js >=24 plus Python 3 stdlib zipfile; local pinned ZIP only"
|
||||
},
|
||||
"inputs": {
|
||||
"editor-data/import-wordnet.mjs": "9b3a668b83d40b5c2659b56c647c40db494d8a9f35fb7ab463b46892ae0d530a",
|
||||
"editor-data/build-thesaurus.mjs": "f1b0b062452fedfb1298f9785f4934046a8b6a0ed8704f5cb82c683d6deb60aa"
|
||||
},
|
||||
"outputs": {
|
||||
"noun.json": {
|
||||
"bytes": 15617486,
|
||||
"sha256": "5294d3f0e6cd7924f5be97d4d6e64b96a9dd8505f216e5c056c3f5b69c0c2dc9"
|
||||
},
|
||||
"verb.json": {
|
||||
"bytes": 2327528,
|
||||
"sha256": "3f99489858d8cbce9996c72135f82325bd2c067a64d2fb8b80dea386b99b6392"
|
||||
},
|
||||
"adj.json": {
|
||||
"bytes": 3319398,
|
||||
"sha256": "9965dbfdc35a11b829fedb19e7b22ab3ca8470f45c5532c1f310810012a23023"
|
||||
},
|
||||
"adv.json": {
|
||||
"bytes": 648351,
|
||||
"sha256": "c6effb7a1502cd3dcd81341cb296e039d238786712c91c30994f2e50ef1e9fcb"
|
||||
},
|
||||
"LICENSE": {
|
||||
"bytes": 1625,
|
||||
"sha256": "7731175a77952e259390b496fab905e57118b8d19ad3a8383c67eee724ff443f"
|
||||
},
|
||||
"NOTICE": {
|
||||
"bytes": 649,
|
||||
"sha256": "508e1a5d132e05ec888a79ec959b68d25203fee0aa3692b0610da7500a7c0251"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 1623,
|
||||
"sha256": "2c58e1513991ea7505ffd8d9dda6e5e8189f8984a7167b051834eb9306e15c72"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Offline WordNet 3.0 -> JSON. No network or runtime/editor integration.
|
||||
// node editor-data/build-thesaurus.mjs [--out DIR] | --measure (with --expose-gc)
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, lstatSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
export const SOURCE = join(root, 'third_party/thesaurus');
|
||||
export const PROVENANCE_SHA256 = 'e5a799a6693321be71c4e99080b9f89ba12a304c3092d05839bb85355194fe76';
|
||||
export const EXPECTED = Object.freeze({ synsets: 117659, headwords: 147306, literalLemmas: 149229 });
|
||||
const POS = { noun: 'n', verb: 'v', adj: 'a', adv: 'r' };
|
||||
const pointerSymbols = new Set('! @ @i ~ ~i #m #s #p %m %s %p = + ;c -c ;r -r ;u -u * > ^ $ & < \\'.split(' '));
|
||||
const sha = b => createHash('sha256').update(b).digest('hex');
|
||||
const json = o => Buffer.from(JSON.stringify(o) + '\n');
|
||||
const normalize = word => word.replace(/\((?:a|p|ip)\)$/, '').toLowerCase();
|
||||
function requireThat(ok, message) { if (!ok) throw new Error(message); }
|
||||
function regular(path) {
|
||||
requireThat(lstatSync(path).isFile(), `source integrity: regular file required: ${path}`);
|
||||
return readFileSync(path);
|
||||
}
|
||||
export function validateSource(sourceRoot = SOURCE) {
|
||||
const provenance = regular(join(sourceRoot, 'PROVENANCE.json'));
|
||||
requireThat(sha(provenance) === PROVENANCE_SHA256, 'provenance integrity mismatch');
|
||||
const m = JSON.parse(provenance);
|
||||
const names = Object.keys(m.files).sort();
|
||||
requireThat(!lstatSync(join(sourceRoot, 'wordnet')).isSymbolicLink(), 'source integrity: symlink directory');
|
||||
requireThat(JSON.stringify(readdirSync(join(sourceRoot, 'wordnet')).sort()) === JSON.stringify(names), 'source inventory mismatch');
|
||||
let total = 0, lookup = 0;
|
||||
for (const name of names) {
|
||||
const b = regular(join(sourceRoot, 'wordnet', name)), pin = m.files[name];
|
||||
requireThat(b.length === pin.bytes && sha(b) === pin.sha256, `source integrity mismatch: ${name}`);
|
||||
total += b.length;
|
||||
if (/^(?:data\.|index\.)|\.exc$/.test(name)) lookup += b.length;
|
||||
}
|
||||
requireThat(total === m.uncompressedBytes && lookup === m.lookupBytes, 'source byte count mismatch');
|
||||
requireThat(regular(join(sourceRoot, 'LICENSE')).equals(regular(join(sourceRoot, 'wordnet/LICENSE'))), 'LICENSE integrity mismatch');
|
||||
const notice = regular(join(sourceRoot, 'NOTICE')).toString('utf8');
|
||||
requireThat(notice.includes('Princeton') && notice.includes('LICENSE'), 'NOTICE missing attribution');
|
||||
return m;
|
||||
}
|
||||
function lines(text, callback) {
|
||||
requireThat(typeof text === 'string' && /^[\x20-\x7e\n]*$/.test(text) && text.endsWith('\n'), 'malformed ASCII source');
|
||||
let offset = 0;
|
||||
for (const line of text.slice(0, -1).split('\n')) {
|
||||
if (!line.startsWith(' ')) { requireThat(line.length > 0, 'malformed blank record'); callback(line, offset); }
|
||||
offset += line.length + 1;
|
||||
}
|
||||
}
|
||||
export function parseData(text, part) {
|
||||
requireThat(Object.hasOwn(POS, part), 'malformed part of speech');
|
||||
const synsets = Object.create(null), pointers = [];
|
||||
lines(text, (line, offset) => {
|
||||
const fail = () => { throw new Error(`malformed data.${part} at byte ${offset}`); };
|
||||
const bar = line.indexOf('|'); if (bar < 0) fail();
|
||||
const f = line.slice(0, bar).trim().split(/ +/); let i = 0;
|
||||
const take = re => { const v = f[i++]; if (!re.test(v ?? '')) fail(); return v; };
|
||||
const id = take(/^\d{8}$/); if (+id !== offset || Object.hasOwn(synsets, id)) fail();
|
||||
const lexFile = +take(/^\d{2}$/); if (lexFile > 44) fail();
|
||||
const pos = take(/^[nvasr]$/); if (pos !== POS[part] && !(part === 'adj' && pos === 's')) fail();
|
||||
const count = parseInt(take(/^[0-9a-f]{2}$/), 16); if (!count) fail();
|
||||
const lemmas = [];
|
||||
for (let n = 0; n < count; n++) { lemmas.push(take(/^\S+$/)); take(/^[0-9a-f]{1,2}$/); }
|
||||
const pcount = +take(/^\d{3}$/);
|
||||
for (let n = 0; n < pcount; n++) {
|
||||
const symbol = take(/^\S+$/); if (!pointerSymbols.has(symbol)) fail();
|
||||
const target = take(/^\d{8}$/), targetPos = take(/^[nvar]$/), words = take(/^[0-9a-f]{4}$/);
|
||||
const from = parseInt(words.slice(0, 2), 16), to = parseInt(words.slice(2), 16);
|
||||
if (from > count || (from === 0) !== (to === 0)) fail();
|
||||
pointers.push({ target, targetPos, to });
|
||||
}
|
||||
if (part === 'verb') {
|
||||
const frames = +take(/^\d{2}$/);
|
||||
for (let n = 0; n < frames; n++) {
|
||||
take(/^\+$/); const frame = +take(/^\d{2}$/), word = parseInt(take(/^[0-9a-f]{2}$/), 16);
|
||||
if (frame < 1 || frame > 35 || word > count) fail();
|
||||
}
|
||||
}
|
||||
const gloss = line.slice(bar + 1).trim(); if (i !== f.length || !gloss) fail();
|
||||
synsets[id] = { pos, lemmas, gloss };
|
||||
});
|
||||
return { synsets, pointers };
|
||||
}
|
||||
export function parseIndex(text, part, synsets) {
|
||||
const index = Object.create(null);
|
||||
lines(text, line => {
|
||||
const f = line.trim().split(/ +/), word = f[0], count = Number(f[2]), pcount = Number(f[3]);
|
||||
const fail = () => { throw new Error(`malformed index.${part}: ${word}`); };
|
||||
if (f[1] !== POS[part] || word !== word.toLowerCase() || Object.hasOwn(index, word)
|
||||
|| !/^\d+$/.test(f[2] ?? '') || !/^\d+$/.test(f[3] ?? '') || count < 1) fail();
|
||||
if (f.length !== 6 + pcount + count || f.slice(4, 4 + pcount).some(p => !pointerSymbols.has(p) && ![';', '-'].includes(p))) fail();
|
||||
const senses = f.slice(6 + pcount);
|
||||
if (+f[4 + pcount] !== count || !/^\d+$/.test(f[5 + pcount]) || +f[5 + pcount] > count || new Set(senses).size !== count) fail();
|
||||
for (const id of senses) {
|
||||
if (!/^\d{8}$/.test(id) || !Object.hasOwn(synsets, id) || !synsets[id].lemmas.some(w => normalize(w) === word)) fail();
|
||||
}
|
||||
index[word] = senses;
|
||||
});
|
||||
// Check the reverse direction too: no lemma/sense silently omitted by the index.
|
||||
for (const [id, s] of Object.entries(synsets)) for (const w of s.lemmas) {
|
||||
requireThat(index[normalize(w)]?.includes(id), `malformed index.${part}: missing ${w}/${id}`);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
function parseExceptions(text, part) {
|
||||
const exceptions = Object.create(null);
|
||||
lines(text, line => {
|
||||
const [word, ...forms] = line.trim().split(/ +/);
|
||||
requireThat(forms.length > 0, `malformed ${part}.exc: ${word}`);
|
||||
// Source exception files can repeat an inflection; preserve all listed bases in order.
|
||||
exceptions[word] = [...new Set([...(exceptions[word] ?? []), ...forms])];
|
||||
});
|
||||
return exceptions;
|
||||
}
|
||||
export function build({ sourceRoot = SOURCE, outDir = join(root, 'src/public/thesaurus'), buildJson = join(outDir, 'BUILD.json') } = {}) {
|
||||
const provenance = validateSource(sourceRoot), parts = {}, parsed = {}, literal = new Set(), normalized = new Set(), headwords = new Set();
|
||||
let synsets = 0;
|
||||
for (const part of Object.keys(POS)) {
|
||||
parsed[part] = parseData(readFileSync(join(sourceRoot, 'wordnet', `data.${part}`), 'utf8'), part);
|
||||
const senses = parsed[part].synsets;
|
||||
const index = parseIndex(readFileSync(join(sourceRoot, 'wordnet', `index.${part}`), 'utf8'), part, senses);
|
||||
parts[part] = { schema: 1, part, synsets: senses, index, exceptions: parseExceptions(readFileSync(join(sourceRoot, 'wordnet', `${part}.exc`), 'utf8'), part) };
|
||||
synsets += Object.keys(senses).length;
|
||||
for (const s of Object.values(senses)) for (const w of s.lemmas) { literal.add(w); normalized.add(normalize(w)); }
|
||||
for (const w of Object.keys(index)) headwords.add(w);
|
||||
}
|
||||
const byPos = Object.fromEntries(Object.entries(POS).map(([p, code]) => [code, parsed[p].synsets]));
|
||||
for (const { pointers } of Object.values(parsed)) for (const p of pointers) {
|
||||
const target = byPos[p.targetPos][p.target];
|
||||
requireThat(target && p.to <= target.lemmas.length, `malformed pointer target: ${p.targetPos}/${p.target}`);
|
||||
}
|
||||
const counts = { synsets, headwords: headwords.size, literalLemmas: literal.size };
|
||||
requireThat(Object.keys(EXPECTED).every(k => counts[k] === EXPECTED[k]), `unexpected counts: ${JSON.stringify(counts)}`);
|
||||
requireThat(normalized.size === headwords.size && [...normalized].every(w => headwords.has(w)), 'normalized data/index headword mismatch');
|
||||
const outputs = {}, bytes = {};
|
||||
for (const [part, data] of Object.entries(parts)) bytes[`${part}.json`] = json(data);
|
||||
bytes.LICENSE = regular(join(sourceRoot, 'LICENSE'));
|
||||
bytes.NOTICE = regular(join(sourceRoot, 'NOTICE'));
|
||||
const countMethod = 'Union of first-field headwords across four index.* files; cross-checked with lowercased data.* lemmas stripping terminal (a)/(p)/(ip). Literal data lemmas counted without normalization.';
|
||||
bytes['manifest.json'] = json({ schema: 1, source: 'Princeton WordNet 3.0', archive: provenance.archive, counts, countMethod,
|
||||
openNote: '28 more union headwords than Princeton statistics page figure 147278; Sage ruling sage-3972 accepts measured 147306 for this archive.',
|
||||
defaults: { maxSenses: 8, maxSynonyms: 20 },
|
||||
format: 'POS shards: index maps lowercase underscore headwords to ordered synset offsets; synsets map offsets to {pos,lemmas,gloss}; exceptions map inflections to bases. Offsets are scoped to their POS shard; s is adjective satellite. Lemmas retain case, underscores and adjective markers; glosses are plain untrusted text. Defaults are consumer hints, not build truncation.',
|
||||
files: Object.fromEntries(Object.entries(bytes).map(([n, b]) => [n, { bytes: b.length, sha256: sha(b) }])) });
|
||||
for (const [n, b] of Object.entries(bytes)) outputs[n] = { bytes: b.length, sha256: sha(b) };
|
||||
const info = { schema: 1, sourceManifestSha256: PROVENANCE_SHA256, archive: provenance.archive, counts, countMethod,
|
||||
openNote: 'Princeton statistics page union 147278 differs by 28; unresolved, accepted in sage-3972.',
|
||||
tools: { node: process.version, initialImportPython: '3.12.8', converter: 'Node.js >=24, standard library only; JSON UTF-8 LF, no timestamps, no locale sorting', importer: 'Node.js >=24 plus Python 3 stdlib zipfile; local pinned ZIP only' },
|
||||
inputs: Object.fromEntries(['editor-data/import-wordnet.mjs', 'editor-data/build-thesaurus.mjs'].map(n => [n, sha(readFileSync(join(root, n)))])), outputs };
|
||||
// No output is touched until all integrity, grammar, references and counts pass.
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
for (const [n, b] of Object.entries(bytes)) writeFileSync(join(outDir, n), b);
|
||||
mkdirSync(dirname(buildJson), { recursive: true });
|
||||
writeFileSync(buildJson, JSON.stringify(info, null, 2) + '\n');
|
||||
return info;
|
||||
}
|
||||
export function measure() {
|
||||
requireThat(typeof global.gc === 'function', 'measurement requires --expose-gc');
|
||||
const files = Object.keys(POS).map(p => `${p}.json`);
|
||||
global.gc(); const before = process.memoryUsage().heapUsed;
|
||||
const data = files.map(n => JSON.parse(readFileSync(join(root, 'src/public/thesaurus', n), 'utf8')));
|
||||
const parsePeak = process.memoryUsage().heapUsed;
|
||||
global.gc(); const after = process.memoryUsage().heapUsed;
|
||||
// Keep every parsed graph live across GC and expose a checkable count.
|
||||
const senses = data.reduce((n, p) => n + Object.keys(p.synsets).length, 0);
|
||||
return { runtime: process.version, platform: process.platform, arch: process.arch, files,
|
||||
method: 'Fresh Node --expose-gc process; GC before/after parsing all four JSON shards; heapUsed delta. parseHeapBytes is a sampled post-parse value, not peak instrumentation; not a browser measurement.',
|
||||
baselineHeapBytes: before, retainedHeapBytes: after - before, parseHeapBytes: parsePeak - before, synsets: senses };
|
||||
}
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 1 && args[0] === '--measure') console.log(JSON.stringify(measure(), null, 2));
|
||||
else {
|
||||
requireThat(args.length === 0 || (args.length === 2 && args[0] === '--out'), 'usage: build-thesaurus.mjs [--out DIR] | --measure');
|
||||
const info = build(args.length ? { outDir: resolve(args[1]) } : { buildJson: join(root, 'editor-data/BUILD.json') });
|
||||
console.log(JSON.stringify(info, null, 2));
|
||||
}
|
||||
} catch (e) { console.error(e.message); process.exitCode = 1; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Offline import only: node editor-data/import-wordnet.mjs /path/to/pinned/wordnet.zip [DEST]
|
||||
// Default DEST is third_party/thesaurus/wordnet, which must not already exist.
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { SOURCE, PROVENANCE_SHA256 } from './build-thesaurus.mjs';
|
||||
export function importWordnet(archive, destination = join(SOURCE, 'wordnet')) {
|
||||
const bytes = readFileSync(archive);
|
||||
const hash = b => createHash('sha256').update(b).digest('hex');
|
||||
if (bytes.length !== 10775600 || hash(bytes) !== 'cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59') throw new Error('archive integrity mismatch; stop and report to Sage');
|
||||
const manifest = readFileSync(join(SOURCE, 'PROVENANCE.json'));
|
||||
if (hash(manifest) !== PROVENANCE_SHA256) throw new Error('provenance integrity mismatch');
|
||||
if (existsSync(destination)) throw new Error('destination already exists; refusing overwrite');
|
||||
// Pass the verified bytes through stdin, avoiding an archive path replacement race.
|
||||
// Validate every entry and its hash before returning any extracted text to Node.
|
||||
const script = `import io,zipfile,sys,json,hashlib
|
||||
m=json.loads(sys.argv[1]); z=zipfile.ZipFile(io.BytesIO(sys.stdin.buffer.read()))
|
||||
entries=[i for i in z.infolist() if not i.is_dir()]
|
||||
expected={'wordnet/'+n for n in m['files']}
|
||||
assert len(entries)==len(expected) and {i.filename for i in entries}==expected, 'archive inventory mismatch'
|
||||
out={}
|
||||
for i in entries:
|
||||
n=i.filename.removeprefix('wordnet/'); pin=m['files'][n]
|
||||
assert i.file_size==pin['bytes'], 'entry size mismatch'
|
||||
b=z.read(i)
|
||||
assert len(b)==pin['bytes'] and hashlib.sha256(b).hexdigest()==pin['sha256'], 'entry hash mismatch'
|
||||
out[n]=b.decode('ascii')
|
||||
print(json.dumps(out))`;
|
||||
const result = spawnSync('python3', ['-c', script, manifest.toString('utf8')], { input: bytes, maxBuffer: 50 * 1024 * 1024 });
|
||||
if (result.error || result.status !== 0) throw new Error(`archive extraction refused: ${result.error?.message ?? result.stderr.toString('utf8')}`);
|
||||
const files = JSON.parse(result.stdout.toString('utf8'));
|
||||
// Exclusive mkdir is the write boundary, also rejecting a concurrent creator.
|
||||
mkdirSync(destination);
|
||||
try { for (const [name, text] of Object.entries(files)) writeFileSync(join(destination, name), text, { encoding: 'ascii', flag: 'wx' }); }
|
||||
catch (e) { rmSync(destination, { recursive: true, force: true }); throw e; }
|
||||
return { files: Object.keys(files).length, bytes: Object.values(files).reduce((n, s) => n + s.length, 0) };
|
||||
}
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
if (process.argv.length < 3 || process.argv.length > 4) throw new Error('usage: import-wordnet.mjs ARCHIVE [DEST]');
|
||||
console.log(JSON.stringify(importWordnet(resolve(process.argv[2]), process.argv[3] ? resolve(process.argv[3]) : undefined)));
|
||||
} catch (e) { console.error(e.message); process.exitCode = 1; }
|
||||
}
|
||||
@@ -18,6 +18,16 @@ export function consoleAccess({ load, business }) {
|
||||
} catch { return null; }
|
||||
};
|
||||
return Object.freeze({
|
||||
// UI hints only: no grants belonging to another user, receipts or subjects escape.
|
||||
chatGrants: session => {
|
||||
try {
|
||||
if (lookup(session.provider, session.sub) !== session.user) return [];
|
||||
const doc = load(), b = business();
|
||||
return doc.grants.filter(g => g.user === session.user && g.business === b &&
|
||||
covers(g, { business: b, project: g.projects[0], instance: g.instances[0] }, new Date(), { level: 'collaborator' }))
|
||||
.map(g => ({ projects: [...g.projects], instances: [...g.instances] }));
|
||||
} catch { return []; }
|
||||
},
|
||||
// The user id this (provider, sub) signs in as, or null.
|
||||
find: ({ provider, sub }) => lookup(provider, sub),
|
||||
// Is this session's user still the holder of its own subject, with a live grant?
|
||||
|
||||
@@ -22,6 +22,7 @@ const STATUS = {
|
||||
'unknown-verb': 503, 'channel-operation': 503,
|
||||
'tester-off': 403, 'tester-required': 403, 'tester-refused': 403,
|
||||
// FED R6 (#1800): the broker's refusals of a granted user.
|
||||
'share-not-covered': 403, 'share-exists': 409, 'share-not-found': 404,
|
||||
'access-refused': 403, 'access-revoked': 401,
|
||||
};
|
||||
function safeMessage(code) {
|
||||
@@ -72,7 +73,7 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site');
|
||||
}
|
||||
try {
|
||||
const match = /^\/api\/chat\/threads\/([^/]+)\/(messages|open|sessions|clear|cancel|operation)$/.exec(path);
|
||||
const match = /^\/api\/chat\/threads\/([^/]+)\/(messages|open|sessions|clear|cancel|operation|shares|share|unshare)$/.exec(path);
|
||||
if (match) {
|
||||
thread = match[1];
|
||||
if (!id(thread)) throw Error();
|
||||
@@ -89,6 +90,9 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
if (!exact(v, ['project', 'name', 'instance'], ['project', 'name']) || !id(v.project) || !name(v.name) ||
|
||||
(Object.hasOwn(v, 'instance') && (typeof v.instance !== 'string' || !KEY.test(v.instance)))) throw Error();
|
||||
action = 'createThread'; args = v;
|
||||
} else if (['share', 'unshare'].includes(match?.[2])) {
|
||||
if (!exact(v, ['to']) || typeof v.to !== 'string' || !v.to.length || v.to.length > 200 || /[\r\n\0]/.test(v.to)) throw Error();
|
||||
action = match[2]; args = { thread, to: v.to };
|
||||
} else if (match?.[2] === 'sessions') {
|
||||
if (!exact(v, ['name', 'op']) || !name(v.name) || !KEY.test(v.op ?? '')) throw Error();
|
||||
action = 'createSession'; args = { thread, name: v.name, op: v.op };
|
||||
@@ -109,9 +113,12 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
} else if (path === '/api/chat/projects') {
|
||||
query(search, []); action = 'projects'; args = {};
|
||||
} else if (path === '/api/chat/threads') {
|
||||
const q = query(search, ['project']);
|
||||
const q = query(search, ['project', 'shared']);
|
||||
if (q.has('shared') && q.get('shared') !== 'true') throw Error();
|
||||
if (q.has('project') && !id(q.get('project'))) throw Error();
|
||||
action = 'threads'; args = q.has('project') ? { project: q.get('project') } : {};
|
||||
action = 'threads'; args = { ...(q.has('project') ? { project: q.get('project') } : {}), ...(q.has('shared') ? { shared: true } : {}) };
|
||||
} else if (match?.[2] === 'shares') {
|
||||
query(search, []); action = 'shares'; args = { thread };
|
||||
} else if (match?.[2] === 'sessions') {
|
||||
query(search, []); action = 'sessions'; args = { thread };
|
||||
} else if (match?.[2] === 'operation') {
|
||||
@@ -155,6 +162,13 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
} : {};
|
||||
return json(res, 200, { rows: value, ...page, at: new Date().toISOString() });
|
||||
}
|
||||
if (action === 'share' || action === 'unshare') {
|
||||
if (value?.thread !== thread || value?.to !== args.to || value?.op !== action) {
|
||||
line('error=outcome-unknown'); return refuse(503, 'outcome-unknown', 'unknown');
|
||||
}
|
||||
line(`thread=${thread} op=${action}`);
|
||||
return json(res, 200, { thread, to: value.to, op: value.op });
|
||||
}
|
||||
if (!value || (action === 'open' ? value.opened !== thread : !id(value.id)) ||
|
||||
(action === 'createThread' && (typeof value.instance !== 'string' || !KEY.test(value.instance)))) {
|
||||
line('error=outcome-unknown');
|
||||
|
||||
@@ -24,7 +24,10 @@ export function createChat({ call, owner = null }) {
|
||||
return Object.freeze({
|
||||
projects: () => channel('project.list', {}),
|
||||
createProject: ({ name, repo }) => channel('project.create', { name, ...(repo === undefined ? {} : { repo }) }),
|
||||
threads: ({ project } = {}) => channel('thread.list', project === undefined ? {} : { project }),
|
||||
threads: ({ project, shared } = {}) => channel('thread.list', { ...(project === undefined ? {} : { project }), ...(shared === true ? { shared: true } : {}) }),
|
||||
shares: ({ thread }) => channel('thread.shares', { thread }),
|
||||
share: ({ thread, to }) => channel('thread.share', { thread, to }),
|
||||
unshare: ({ thread, to }) => channel('thread.unshare', { thread, to }),
|
||||
createThread: ({ project, name, instance }) => channel('thread.create', { project, name, ...(instance === undefined ? {} : { instance }) }),
|
||||
sessions: ({ thread }) => channel('session.list', { thread }),
|
||||
createSession: ({ thread, name, op }) => channel('session.create', { thread, name, op }),
|
||||
|
||||
@@ -1232,7 +1232,7 @@
|
||||
window.MosaicChat?.leave(path);
|
||||
if (path === '/chat' || path.startsWith('/chat/')) {
|
||||
// Row 177 (#1675): the tester Console writes to its own threads.
|
||||
await window.MosaicChat.render({ path, container: $('bus-view'), canSend: canSend || isTester, navigated });
|
||||
await window.MosaicChat.render({ path, container: $('bus-view'), canSend: canSend || isTester, tester: isTester, navigated });
|
||||
if (g !== gen || out()) return;
|
||||
shown = route; schedule(); return;
|
||||
}
|
||||
|
||||
@@ -57,23 +57,35 @@
|
||||
const disabledSession = pending => disabled(pending || selectedInactive());
|
||||
const disabled = pending => !current.canSend || out() || pending ? ' disabled' : '';
|
||||
const stamp = at => `<time>${txt(at)}</time>`;
|
||||
// Per-grant UI hints; all writes still use the caller's bus principal.
|
||||
const scopeWrites = (project, instance) => !current.user || current.chatGrants.some(g =>
|
||||
(g.projects.includes('*') || g.projects.includes(project)) &&
|
||||
(instance === undefined || g.instances.includes('*') || g.instances.includes(instance)));
|
||||
const threadWrites = () => !!current.thread && !current.shared && scopeWrites(current.thread?.project, current.thread?.instance);
|
||||
function sharing(t) {
|
||||
if (!current.canSend || !threadWrites() || current.tester) return '';
|
||||
const f = form('share:' + t.id);
|
||||
return `<section aria-label="Thread sharing"><h2>Share read-only</h2><p>Share with a user (user:id), the business owner's name, or * for everyone granted on this thread.</p><ul>${(current.shares ?? []).map(r => `<li>${txt(r.to)} <button class="btn" type="button" data-unshare="${esc(r.to)}"${disabled(f.pending)}>Unshare</button></li>`).join('')}</ul><form id="chat-share-form"><label>Share with<input id="chat-share-target" value="${esc(f.to ?? '')}" maxlength="200" required></label><button class="btn" type="submit"${disabled(f.pending)}>Share read-only</button></form><p role="status">${txt(f.receipt)}</p></section>`;
|
||||
}
|
||||
function creation(kind, key) {
|
||||
const f = form(key), project = kind === 'project';
|
||||
if (current.user && (project || !scopeWrites(key))) return '';
|
||||
const repo = project && current.repos.length ? `<label>Repository (optional)<select id="chat-repo" data-chat-field="repo"><option value="">No repository link</option>${current.repos.map(r => `<option value="${esc(r)}"${r === f.repo ? ' selected' : ''}>${txt(r)}</option>`).join('')}</select></label>` : '';
|
||||
const choices = current.instances ?? [];
|
||||
const choices = (current.instances ?? []).filter(r => scopeWrites(key, r.instance));
|
||||
const picker = project ? '' : `<label>Thread with<select id="chat-instance" data-chat-field="instance" required${disabled(f.pending || !!current.instanceError || !choices.length)}><option value="">Choose who this thread is with</option>${choices.map(row => `<option value="${esc(row.instance)}"${row.instance === f.instance ? ' selected' : ''}>${txt(row.name === row.instance ? row.instance : `${row.name} (${row.instance})`)}</option>`).join('')}</select></label>`;
|
||||
const unavailable = !project && (!!current.instanceError || !choices.some(row => row.instance === f.instance));
|
||||
const status = !project && current.instanceError ? current.instanceError : f.receipt || (!project && !choices.length ? 'No thread instances are configured.' : '');
|
||||
return `<form id="chat-${kind}-form" class="chat-create" data-key="${esc(key)}"><h2>New ${kind}</h2><label>Name<input id="chat-${kind}-name" data-chat-field="name" value="${esc(f.name)}" maxlength="200" required${disabled(f.pending)}></label>${repo}${picker}<button class="btn" type="submit"${disabled(f.pending || unavailable)}>${f.pending ? 'Creating…' : `Create ${kind}`}</button><p role="status">${txt(status)}</p></form>`;
|
||||
}
|
||||
function threads(rows) {
|
||||
return rows.length ? `<ul class="chat-thread-list">${rows.map(t => `<li>${link('threads', t.id, t.name)} <span class="muted">${txt(t.instance)}</span></li>`).join('')}</ul>` : '<p>No threads yet.</p>';
|
||||
return rows.length ? `<ul class="chat-thread-list">${rows.map(t => `<li>${link('threads', t.id, t.name)} <span class="muted">${txt(t.instance)}${t.shared ? ` · Shared by ${txt(t.owner)}` : ''}</span></li>`).join('')}</ul>` : '<p>No threads yet.</p>';
|
||||
}
|
||||
const sessionPath = (thread, session) => `#/chat/threads/${thread}/sessions/${session}`;
|
||||
const countLabel = value => !value ? 'unavailable (count collector not installed)' :
|
||||
value.known === true && Number.isSafeInteger(value.count) && value.count >= 0 ? String(value.count) :
|
||||
`unknown (${value.reason ?? 'not-collected'})`;
|
||||
function sessionControls(t) {
|
||||
if (!threadWrites()) return '<p>Read-only thread.</p>';
|
||||
if (current.legacyDefault) return '<p role="status">Sessions are not available for this account.</p>';
|
||||
const c = current, selected = c.sessions.find(r => r.id === c.session), f = form('session:' + t.id);
|
||||
const clear = form('clear:' + c.key);
|
||||
@@ -84,7 +96,7 @@
|
||||
const s = state(current.key);
|
||||
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div><div class="chat-body">${txt(m.body)}</div></li>`).join('');
|
||||
const receipt = s.receipt;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${sessionControls(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button><ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div><form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form><p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${sessionControls(t)}${sharing(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p>${threadWrites() ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button>` : ''}<ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div>${threadWrites() ? `<form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form>` : ''}<p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
}
|
||||
function capture() {
|
||||
if (!current) return null;
|
||||
@@ -98,16 +110,16 @@
|
||||
let html = '<div class="chat-area"><p><a href="#/chat">Chat projects</a></p>';
|
||||
if (c.error) html += `<p class="chat-error banner err" role="alert">${txt(c.error)}</p>`;
|
||||
if (c.kind === 'thread') {
|
||||
const t = c.threads.find(t => t.id === c.id);
|
||||
const t = c.thread;
|
||||
if (t && (c.session || c.legacyDefault) && !c.refused) html += conversation(t);
|
||||
else html += heading('Thread unavailable');
|
||||
} else if (c.kind === 'project') {
|
||||
const p = c.projects.find(p => p.id === c.id);
|
||||
html += heading(p?.name ?? 'Project unavailable');
|
||||
if (p && !c.refused) html += threads(c.threads) + creation('thread', c.id);
|
||||
if (p && !c.refused) html += threads(c.threads.filter(t => !t.shared)) + '<h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('thread', c.id);
|
||||
} else {
|
||||
html += heading('Chat') + '<p>Business projects and your named assistant threads.</p><div class="chat-projects">' +
|
||||
c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id))}</section>`).join('') + (c.projects.length ? '' : '<p>No projects yet.</p>') + '</div>' + creation('project', 'projects');
|
||||
c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id && !t.shared))}</section>`).join('') + (c.projects.length ? '' : '<p>No projects yet.</p>') + '</div><h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('project', 'projects');
|
||||
}
|
||||
if (!c.canSend) html += '<p class="muted">Creating and sending require signed-in send mode. This Console only reads.</p>';
|
||||
c.container.innerHTML = html + '</div>';
|
||||
@@ -132,7 +144,7 @@
|
||||
err.code === 'thread-not-found' ? 'Thread not found.' :
|
||||
err.code === 'session-not-found' ? 'Session not found in this thread.' : 'Chat could not be read. Refresh to try again; your draft is kept.';
|
||||
}
|
||||
async function render({ path, container, canSend, navigated = false, next = false }) {
|
||||
async function render({ path, container, canSend, user = false, tester = false, navigated = false, next = false }) {
|
||||
if (out()) return;
|
||||
const match = /^\/chat(?:\/(projects|threads)\/([0-9a-f-]+)(?:\/sessions\/([0-9a-f-]+))?)?$/.exec(path);
|
||||
controller?.abort(); controller = new AbortController();
|
||||
@@ -144,23 +156,46 @@
|
||||
paint(); return;
|
||||
}
|
||||
const old = current;
|
||||
current = { container, kind, id, canSend, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', refused: false, legacyDefault: false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
current = { container, kind, id, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', refused: false, legacyDefault: false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
|
||||
try {
|
||||
if (user) {
|
||||
const config = await fetch('/api/config', { cache: 'no-store', redirect: 'manual', signal });
|
||||
if (config.type === 'opaqueredirect' || config.status === 401) { signOut(); return; }
|
||||
if (!config.ok) throw new ChatError('invalid-response');
|
||||
const hints = await config.json();
|
||||
if (g !== generation || out()) return;
|
||||
current.chatGrants = Array.isArray(hints.chatGrants) ? hints.chatGrants : [];
|
||||
current.canSend = hints.send === true;
|
||||
}
|
||||
// Resolve the explicit session before touching history/cursors. Missing selectors never
|
||||
// silently fall back to a sibling. The saved default is used only for thread-only routes.
|
||||
const [projects, listed, catalog] = await Promise.all([
|
||||
const [projects, listed, shared] = await Promise.all([
|
||||
request('projects', undefined, signal),
|
||||
request('threads' + (kind === 'project' ? '?project=' + id : ''), undefined, signal),
|
||||
kind === 'thread' ? request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
// A denied optional catalog does not revoke an existing default-thread grant.
|
||||
// Explicit selectors and every other failure keep their refusal/error state.
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
}) : null,
|
||||
tester ? { rows: [] } : request('threads?shared=true' + (kind === 'project' ? '&project=' + id : ''), undefined, signal).catch(err => {
|
||||
if (['signed-out', 'access-revoked'].includes(err.code)) throw err;
|
||||
return { rows: [] };
|
||||
}),
|
||||
]);
|
||||
if (g !== generation || out()) return;
|
||||
current.projects = projects.rows; current.threads = listed.rows;
|
||||
current.projects = projects.rows;
|
||||
const own = new Set(listed.rows.map(t => t.id));
|
||||
current.threads = [...listed.rows, ...shared.rows.filter(t => !own.has(t.id)).map(t => ({ ...t, shared: true }))];
|
||||
current.thread = current.threads.find(t => t.id === id);
|
||||
current.shared = current.thread?.shared === true;
|
||||
const catalog = kind === 'thread' && !current.shared ? await request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
}) : null;
|
||||
if (g !== generation || out()) return;
|
||||
if (current.shared && match[3]) throw new ChatError('session-not-found', 404);
|
||||
current.shares = [];
|
||||
if (kind === 'thread' && current.canSend && threadWrites() && !tester) {
|
||||
const shares = await request(`threads/${id}/shares`, undefined, signal).catch(() => ({ rows: [] }));
|
||||
if (g !== generation || out()) return;
|
||||
current.shares = shares.rows;
|
||||
}
|
||||
let s = null, page = null, after = null;
|
||||
if (kind === 'thread' && catalog === null) {
|
||||
current.legacyDefault = true; current.sessions = []; current.session = null;
|
||||
@@ -208,13 +243,13 @@
|
||||
if (g !== generation || out()) return;
|
||||
if (!listed.rows.every(row => typeof row?.instance === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(row.instance) && typeof row.name === 'string'))
|
||||
throw new ChatError('invalid-response');
|
||||
current.instances = listed.rows; current.instanceError = '';
|
||||
current.instances = listed.rows.filter(r => scopeWrites(id, r.instance)); current.instanceError = '';
|
||||
const f = form(id);
|
||||
if (f.instance && !listed.rows.some(row => row.instance === f.instance)) {
|
||||
if (f.instance && !current.instances.some(row => row.instance === f.instance)) {
|
||||
f.instance = ''; f.instanceChosen = true;
|
||||
f.receipt = 'The selected instance is no longer available. Choose who this thread is with.';
|
||||
}
|
||||
if (!f.instanceChosen && !f.instance && listed.rows.length === 1) f.instance = listed.rows[0].instance;
|
||||
if (!f.instanceChosen && !f.instance && current.instances.length === 1) f.instance = current.instances[0].instance;
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError' || out() || g !== generation) return;
|
||||
current.instances = [];
|
||||
@@ -244,7 +279,7 @@
|
||||
for (const key of forms.keys()) if (key.includes(id)) forms.delete(key);
|
||||
}
|
||||
if (err.status === 403 || err.status === 404) {
|
||||
current.projects = []; current.threads = []; current.refused = true;
|
||||
current.projects = []; current.threads = []; current.thread = null; current.shared = false; current.refused = true;
|
||||
}
|
||||
paint(navigated);
|
||||
}
|
||||
@@ -258,6 +293,7 @@
|
||||
document.addEventListener('input', event => {
|
||||
if (!current || out()) return;
|
||||
if (event.target.id === 'chat-operation') { const f = form('clear:' + current.key); if (f.pending) return; f.op = event.target.value; f.record = null; const button = document.getElementById('chat-cancel'); if (button) button.disabled = !current.canSend || f.pending || !f.op; }
|
||||
if (event.target.id === 'chat-share-target') form('share:' + current.id).to = event.target.value;
|
||||
if (event.target.id === 'chat-body') state(current.key ?? current.id).draft = event.target.value;
|
||||
const f = event.target.closest('.chat-create');
|
||||
if (f && event.target.dataset.chatField) {
|
||||
@@ -275,6 +311,8 @@
|
||||
async function write(action, key, value, path) {
|
||||
if (!current || !current.canSend || out()) return;
|
||||
const isThread = action === 'send' || action === 'open';
|
||||
if ((isThread || action === 'createSession') && !threadWrites()) return;
|
||||
if (action === 'createProject' && current.user || action === 'createThread' && !scopeWrites(value.project, value.instance)) return;
|
||||
const targetThread = current.id;
|
||||
const s = isThread ? state(key) : form(key);
|
||||
if (s.pending || isThread && selectedInactive()) return;
|
||||
@@ -314,7 +352,9 @@
|
||||
document.addEventListener('submit', event => {
|
||||
if (!current || !event.target.id.startsWith('chat-')) return;
|
||||
event.preventDefault();
|
||||
if (event.target.id === 'chat-send-form') {
|
||||
if (event.target.id === 'chat-share-form') {
|
||||
void changeShare('share', form('share:' + current.id).to ?? '');
|
||||
} else if (event.target.id === 'chat-send-form') {
|
||||
const id = current.id, session = current.session, key = current.key;
|
||||
void write('send', key, { body: state(key).draft, ...(session ? { session } : {}) }, `threads/${id}/messages`);
|
||||
} else if (event.target.id === 'chat-session-form') {
|
||||
@@ -336,6 +376,7 @@
|
||||
});
|
||||
document.addEventListener('click', event => {
|
||||
if (!current || out()) return;
|
||||
if (event.target.dataset.unshare) void changeShare('unshare', event.target.dataset.unshare);
|
||||
if (event.target.id === 'chat-resume') void write('open', current.key, current.session ? { session: current.session } : {}, `threads/${current.id}/open`);
|
||||
if (event.target.id === 'chat-cancel') void cancelClear();
|
||||
if (event.target.id === 'chat-clear') void clearContext();
|
||||
@@ -347,6 +388,23 @@
|
||||
location.hash = sessionPath(current.id, event.target.value);
|
||||
}
|
||||
});
|
||||
async function changeShare(op, to) {
|
||||
if (!current?.canSend || !threadWrites() || current.tester || out()) return;
|
||||
const id = current.id, f = form('share:' + id);
|
||||
if (f.pending || !to.trim()) return;
|
||||
f.pending = true; f.receipt = ''; paint();
|
||||
try {
|
||||
const r = await request(`threads/${id}/${op}`, { to });
|
||||
if (r.thread !== id || r.to !== to || r.op !== op) throw new ChatError('outcome-unknown');
|
||||
f.receipt = op === 'share' ? 'Shared read-only.' : 'Share ended.';
|
||||
if (op === 'share') f.to = '';
|
||||
} catch (e) {
|
||||
f.receipt = e.sent === false ? `Share refused (${inert(e.code)}).` : 'Outcome unknown. Read shares before trying again; nothing was retried.';
|
||||
} finally {
|
||||
f.pending = false;
|
||||
if (!out() && current?.id === id) { await refresh(); paint(); }
|
||||
}
|
||||
}
|
||||
const clearText = r => r.state === 'cancelled' ? `Operation ${r.op}: ${r.retired === true ? 'startup cancelled; old session remains retired' : 'replacement cancelled; old session active'}. Cancelled inputs are not replayed.` : `Operation ${r.op}: ${r.state}; successor ${r.successor}; start ${r.start ?? 'pending'}${r.refusal ? '; ' + r.refusal : ''}. No stop is forced.`;
|
||||
async function checkClear(thread, key, f) {
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) { f.receipt = 'Enter a valid operation key.'; paint(); return; }
|
||||
@@ -361,7 +419,7 @@
|
||||
}
|
||||
async function cancelClear() {
|
||||
const c = current, key = c.key, f = form('clear:' + key);
|
||||
if (!c.canSend || out() || f.pending || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) return;
|
||||
if (!c.canSend || !threadWrites() || out() || f.pending || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) return;
|
||||
if (!window.confirm(`Cancel clear operation ${f.op}? Before retirement this restores the old session to active. After retirement it only cancels successor startup. Cancelled messages are never replayed, and nothing is un-retired.`)) return;
|
||||
const op = f.op;
|
||||
f.pending = true; paint();
|
||||
@@ -379,7 +437,7 @@
|
||||
}
|
||||
async function clearContext() {
|
||||
const c = current, key = c.key, selected = c.sessions.find(r => r.id === c.session), f = form('clear:' + key);
|
||||
if (!c.canSend || out() || f.pending || selectedInactive() || !Number.isSafeInteger(selected?.revision)) return;
|
||||
if (!c.canSend || !threadWrites() || out() || f.pending || selectedInactive() || !Number.isSafeInteger(selected?.revision)) return;
|
||||
if (!window.confirm(`Clear context for "${selected.name}"? This ends this session after a safe stop and starts fresh context. History and the old checkout (including unpushed work) are retained. The successor uses a fresh approved checkout; old work is not copied. This can wait for safe stop or capacity.`)) return;
|
||||
// A lost answer retains this key. Only status reads are allowed afterward.
|
||||
if (f.op && !['cancelled', 'failed'].includes(f.record?.state)) { await checkClear(c.id, key, f); return; }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
WordNet Release 3.0
|
||||
|
||||
This software and database is being provided to you, the LICENSEE, by
|
||||
Princeton University under the following license. By obtaining, using
|
||||
and/or copying this software and database, you agree that you have
|
||||
read, understood, and will comply with these terms and conditions.:
|
||||
|
||||
Permission to use, copy, modify and distribute this software and
|
||||
database and its documentation for any purpose and without fee or
|
||||
royalty is hereby granted, provided that you agree to comply with
|
||||
the following copyright notice and statements, including the disclaimer,
|
||||
and that the same appear on ALL copies of the software, database and
|
||||
documentation, including modifications that you make for internal
|
||||
use or for distribution.
|
||||
|
||||
WordNet 3.0 Copyright 2006 by Princeton University. All rights reserved.
|
||||
|
||||
THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON
|
||||
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PRINCETON
|
||||
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES OF MERCHANT-
|
||||
ABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE
|
||||
OF THE LICENSED SOFTWARE, DATABASE OR DOCUMENTATION WILL NOT
|
||||
INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR
|
||||
OTHER RIGHTS.
|
||||
|
||||
The name of Princeton University or Princeton may not be used in
|
||||
advertising or publicity pertaining to distribution of the software
|
||||
and/or database. Title to copyright in this software, database and
|
||||
any associated documentation shall at all times remain with
|
||||
Princeton University and LICENSEE agrees to preserve same.
|
||||
@@ -0,0 +1,11 @@
|
||||
Princeton WordNet 3.0
|
||||
WordNet 3.0 Copyright 2006 by Princeton University. All rights reserved.
|
||||
|
||||
Original database text is retained unmodified in wordnet/.
|
||||
Derived JSON is produced by editor-data/build-thesaurus.mjs, with underscores
|
||||
and adjective position markers retained in sense lemmas, and a lowercase index.
|
||||
This database and derived data remain subject to the Princeton terms and
|
||||
disclaimer in LICENSE. They are not licensed under MIT or Apache.
|
||||
|
||||
Pinned NLTK distribution mirror (not a comparison with a Princeton archive):
|
||||
https://raw.githubusercontent.com/nltk/nltk_data/96f9b3252457a2b97e52aec64c3dfceeb5c312d5/packages/corpora/wordnet.zip
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":1,"part":"adj","synsets":{"00001740":{"pos":"a","lemmas":["able"],"gloss":"(usually followed by `to') having the necessary means or skill or know-how or authority to do something; \"able to swim\"; \"she was able to program her computer\"; \"we were at last able to buy a car\"; \"able to get a grant for the project\""},"00002098":{"pos":"a","lemmas":["unable"],"gloss":"(usually followed by `to') not having the necessary means or skill or know-how; \"unable to get to town without a car\"; \"unable to obtain funds\""},"00002312":{"pos":"a","lemmas":["abaxial","dorsal"],"gloss":"facing away from the axis of an organ or organism; \"the abaxial surface of a leaf is the underside or side facing away from the stem\""},"00002527":{"pos":"a","lemmas":["adaxial","ventral"],"gloss":"nearest to or facing toward the axis of an organ or organism; \"the upper side of a leaf is known as the adaxial surface\""},"00002730":{"pos":"a","lemmas":["acroscopic"],"gloss":"facing or on the side toward the apex"},"00002843":{"pos":"a","lemmas":["basiscopic"],"gloss":"facing or on the side toward the base"},"00002956":{"pos":"a","lemmas":["abducent","abducting"],"gloss":"especially of muscles; drawing away from the midline of the body or from an adjacent part"},"00003131":{"pos":"a","lemmas":["adducent","adductive","adducting"],"gloss":"especially of muscles; bringing together or drawing toward the midline of the body or toward an adjacent part"},"00003356":{"pos":"a","lemmas":["nascent"],"gloss":"being born or beginning; \"the nascent chicks\"; \"a nascent insurgency\""},"00003553":{"pos":"s","lemmas":["emergent","emerging"],"gloss":"coming into existence; \"an emergent republic\""},"00003700":{"pos":"s","lemmas":["dissilient"],"gloss":"bursting open with force, as do some ripe seed vessels"},"00003829":{"pos":"s","lemmas":["parturient"],"gloss":"giving birth; \"a parturient heifer\""},"00003939":{"pos":"a","lemmas":["dying"],"gloss":"in or associated with the process of passing from life or ceasing to be; \"a dying man\"; \"his dying wish\"; \"a dying fire\"; \"a dying civilization\""},"00004171":{"pos":"s","lemmas":["moribund"],"gloss":"being on the point of death; breathing your last; \"a moribund patient\""},"00004296":{"pos":"s","lemmas":["last"],"gloss":"occurring at the time of death; \"his last words\"; \"the last rites\""},"00004413":{"pos":"a","lemmas":["abridged"],"gloss":"(used of texts) shortened by condensing or rewriting; \"an abridged version\""},"00004615":{"pos":"s","lemmas":["cut","shortened"],"gloss":"with parts removed; \"the drastically cut film\""},"00004723":{"pos":"s","lemmas":["half-length"],"gloss":"abridged to half its original length"},"00004817":{"pos":"s","lemmas":["potted"],"gloss":"(British informal) summarized or abridged; \"a potted version of a novel\""},"00004980":{"pos":"a","lemmas":["unabridged"],"gloss":"(used of texts) not shortened; \"an unabridged novel\""},"00005107":{"pos":"s","lemmas":["full-length","uncut"],"gloss":"complete; \"the full-length play\""},"00005205":{"pos":"a","lemmas":["absolute"],"gloss":"perfect or complete or pure; \"absolute loyalty\"; \"absolute silence\"; \"absolute truth\"; \"absolute alcohol\""},"00005473":{"pos":"s","lemmas":["direct"],"gloss":"lacking compromising or mitigating elements; exact; \"the direct opposite\""},"00005599":{"pos":"s","lemmas":["implicit","unquestioning"],"gloss":"being without doubt or reserve; \"implicit trust\""},"00005718":{"pos":"s","lemmas":["infinite"],"gloss":"total and all-embracing; \"God's infinite wisdom\""},"00005839":{"pos":"s","lemmas":["living"],"gloss":"(informal) absolute; \"she is a living doll\"; \"scared the living daylights out of them\"; \"beat the living hell out of him\""},"00006032":{"pos":"a","lemmas":["relative","comparative"],"gloss":"estimated by comparison; not absolute or complete; \"a relative stranger\""},"00006245":{"pos":"s","lemmas":["relational"],"gloss":"having a relation or being related"},"00006336":{"pos":"a","lemmas":["absorbent","absorptive"],"gloss":"having power or capacity or tendency to absorb or soak up something (liquids or energy etc.); \"as absorbent as a sponge\""},"00006777":{"pos":"s","lemmas":["absorbefacient","sorbefacient"],"gloss":"inducing or promoting absorption"},"00006885":{"pos":"s","lemmas":["assimilating","assimilative","assimilatory"],"gloss":"capable of taking (gas, light, or liquids) into a solution; \"an assimilative substance"},"00007096":{"pos":"s","lemmas":["hygroscopic"],"gloss":"absorbing moisture (as from the air)"},"00007208":{"pos":"s","lemmas":["receptive"],"gloss":"able to absorb liquid (not repellent); \"the paper is ink-receptive\""},"00007331":{"pos":"s","lemmas":["shock-absorbent"],"gloss":"having the capacity to absorb the energy of an impact; \"the material absorbs shock and is used for shock-absorbent insoles\""},"00007516":{"pos":"s","lemmas":["spongy","spongelike"],"gloss":"like a sponge in being able to absorb liquids and yield it back wheLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":1,"part":"adv","synsets":{"00001740":{"pos":"r","lemmas":["a_cappella"],"gloss":"without musical accompaniment; \"they performed a cappella\""},"00001837":{"pos":"r","lemmas":["AD","A.D.","anno_Domini"],"gloss":"in the Christian era; used before dates after the supposed year Christ was born; \"in AD 200\""},"00001981":{"pos":"r","lemmas":["CE","C.E.","Common_Era"],"gloss":"of the period coinciding with the Christian era; preferred by some writers who are not Christians; \"in 200 CE\""},"00002142":{"pos":"r","lemmas":["BC","B.C.","before_Christ"],"gloss":"before the Christian era; used following dates before the supposed year Christ was born; \"in 200 BC\""},"00002296":{"pos":"r","lemmas":["BCE","B.C.E."],"gloss":"of the period before the Common Era; preferred by some writers who are not Christians; \"in 200 BCE\""},"00002436":{"pos":"r","lemmas":["horseback","ahorse","ahorseback"],"gloss":"on the back of a horse; \"he rode horseback to town\"; \"managed to escape ahorse\"; \"policeman patrolled the streets ahorseback\""},"00002621":{"pos":"r","lemmas":["barely","hardly","just","scarcely","scarce"],"gloss":"only a very short time before; \"they could barely hear the speaker\"; \"we hardly knew them\"; \"just missed being hit\"; \"had scarcely rung the bell when the door flew open\"; \"would have scarce arrived before she would have found some excuse to leave\"- W.B.Yeats"},"00002950":{"pos":"r","lemmas":["just"],"gloss":"exactly at this moment or the moment described; \"we've just finished painting the walls, so don't touch them\";"},"00003093":{"pos":"r","lemmas":["hardly","scarcely"],"gloss":"almost not; \"he hardly ever goes fishing\"; \"he was hardly more than sixteen years old\"; \"they scarcely ever used the emergency generator\""},"00003294":{"pos":"r","lemmas":["anisotropically"],"gloss":"in an anisotropic manner"},"00003380":{"pos":"r","lemmas":["annoyingly"],"gloss":"in an annoying manner or to an annoying degree"},"00003483":{"pos":"r","lemmas":["basically","fundamentally","essentially"],"gloss":"in essence; at bottom or by one's (or its) very nature; \"He is basically dishonest\"; \"the argument was essentially a technical one\"; \"for all his bluster he is in essence a shy person\""},"00003771":{"pos":"r","lemmas":["blessedly"],"gloss":"in a blessed manner"},"00003846":{"pos":"r","lemmas":["boiling"],"gloss":"extremely; \"boiling mad\""},"00003925":{"pos":"r","lemmas":["enviably"],"gloss":"in an enviable manner; \"she was enviably fluent in French\""},"00004038":{"pos":"r","lemmas":["pointedly"],"gloss":"in such a manner as to make something clearly evident; \"he pointedly ignored the question\""},"00004184":{"pos":"r","lemmas":["negatively"],"gloss":"in a negative way; \"he was negatively inclined\""},"00004288":{"pos":"r","lemmas":["negatively"],"gloss":"in a harmful manner; \"he was negatively affected\""},"00004394":{"pos":"r","lemmas":["kindly"],"gloss":"in a kind manner or out of kindness; \"He spoke kindly to the boy\"; \"she kindly overlooked the mistake\""},"00004567":{"pos":"r","lemmas":["unkindly"],"gloss":"in an unkind manner or with unkindness; \"The teacher treats the children unkindly\""},"00004722":{"pos":"r","lemmas":["merely","simply","just","only","but"],"gloss":"and nothing more; \"I was merely asking\"; \"it is simply a matter of time\"; \"just a scratch\"; \"he was only a child\"; \"hopes that last but a moment\""},"00004967":{"pos":"r","lemmas":["simply"],"gloss":"absolutely; altogether; really; \"we are simply broke\""},"00005055":{"pos":"r","lemmas":["plainly","simply"],"gloss":"in a simple manner; without extravagance or embellishment; \"she was dressed plainly\"; \"they lived very simply\""},"00005210":{"pos":"r","lemmas":["anciently"],"gloss":"in ancient times; long ago; \"a concern with what may have happened anciently\""},"00005343":{"pos":"r","lemmas":["arguably"],"gloss":"as can be shown by argument; \"she is arguably the best\""},"00005453":{"pos":"r","lemmas":["unabashedly"],"gloss":"in an unabashed manner; \"unabashedly, he asked for more\""},"00005567":{"pos":"r","lemmas":["automatically"],"gloss":"in a reflex manner; \"he answered automatically\""},"00005674":{"pos":"r","lemmas":["alarmingly"],"gloss":"in an alarming manner; \"It grew alarmingly fast\""},"00005779":{"pos":"r","lemmas":["vastly","immensely"],"gloss":"to an exceedingly great extent or degree; \"He had vastly overestimated his resources\"; \"was immensely more important to the project as a scientist than as an administrator\""},"00006034":{"pos":"r","lemmas":["grossly"],"gloss":"in a gross manner"},"00006105":{"pos":"r","lemmas":["largely","mostly","for_the_most_part"],"gloss":"in large part; mainly or chiefly; \"These accounts are largely inactive\""},"00006259":{"pos":"r","lemmas":["significantly"],"gloss":"in a significant manner; \"our budget will be significantly affected by these new cuts\""},"00006423":{"pos":"r","lemmas":["insignificantly"],"gloss":"not to a significant degreeLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":1,"source":"Princeton WordNet 3.0","archive":{"bytes":10775600,"sha256":"cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59"},"counts":{"synsets":117659,"headwords":147306,"literalLemmas":149229},"countMethod":"Union of first-field headwords across four index.* files; cross-checked with lowercased data.* lemmas stripping terminal (a)/(p)/(ip). Literal data lemmas counted without normalization.","openNote":"28 more union headwords than Princeton statistics page figure 147278; Sage ruling sage-3972 accepts measured 147306 for this archive.","defaults":{"maxSenses":8,"maxSynonyms":20},"format":"POS shards: index maps lowercase underscore headwords to ordered synset offsets; synsets map offsets to {pos,lemmas,gloss}; exceptions map inflections to bases. Offsets are scoped to their POS shard; s is adjective satellite. Lemmas retain case, underscores and adjective markers; glosses are plain untrusted text. Defaults are consumer hints, not build truncation.","files":{"noun.json":{"bytes":15617486,"sha256":"5294d3f0e6cd7924f5be97d4d6e64b96a9dd8505f216e5c056c3f5b69c0c2dc9"},"verb.json":{"bytes":2327528,"sha256":"3f99489858d8cbce9996c72135f82325bd2c067a64d2fb8b80dea386b99b6392"},"adj.json":{"bytes":3319398,"sha256":"9965dbfdc35a11b829fedb19e7b22ab3ca8470f45c5532c1f310810012a23023"},"adv.json":{"bytes":648351,"sha256":"c6effb7a1502cd3dcd81341cb296e039d238786712c91c30994f2e50ef1e9fcb"},"LICENSE":{"bytes":1625,"sha256":"7731175a77952e259390b496fab905e57118b8d19ad3a8383c67eee724ff443f"},"NOTICE":{"bytes":649,"sha256":"508e1a5d132e05ec888a79ec959b68d25203fee0aa3692b0610da7500a7c0251"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":1,"part":"noun","synsets":{"10000007":{"pos":"n","lemmas":["defecator","voider","shitter"],"gloss":"a person who defecates"},"10000158":{"pos":"n","lemmas":["defense_attorney","defense_lawyer"],"gloss":"the lawyer representing the defendant"},"10000294":{"pos":"n","lemmas":["defense_contractor"],"gloss":"a contractor concerned with the development and manufacture of systems of defense"},"10000459":{"pos":"n","lemmas":["deist","freethinker"],"gloss":"a person who believes that God created the universe and then abandoned it"},"10000616":{"pos":"n","lemmas":["delayer"],"gloss":"a person who delays; to put off until later or cause to be late"},"10000787":{"pos":"n","lemmas":["delegate"],"gloss":"a person appointed or elected to represent others"},"10000945":{"pos":"n","lemmas":["delinquent","juvenile_delinquent"],"gloss":"a young offender"},"10001058":{"pos":"n","lemmas":["deliverer"],"gloss":"a person who gives up or transfers money or goods"},"10001217":{"pos":"n","lemmas":["deliveryman","delivery_boy","deliverer"],"gloss":"someone employed to make deliveries"},"10001481":{"pos":"n","lemmas":["demagogue","demagog","rabble-rouser"],"gloss":"a political leader who seeks support by appealing to popular passions and prejudices"},"10001647":{"pos":"n","lemmas":["demander"],"gloss":"a person who makes demands"},"10001764":{"pos":"n","lemmas":["demigod","superman","Ubermensch"],"gloss":"a person with great powers and abilities"},"10001882":{"pos":"n","lemmas":["demimondaine"],"gloss":"a woman whose sexual promiscuity places her outside respectable society"},"10002031":{"pos":"n","lemmas":["democrat","populist"],"gloss":"an advocate of democratic principles"},"10002151":{"pos":"n","lemmas":["Democrat"],"gloss":"a member of the Democratic Party"},"10002257":{"pos":"n","lemmas":["demographer","demographist","population_scientist"],"gloss":"a scientist who studies the growth and density of populations and their vital statistics"},"10002477":{"pos":"n","lemmas":["demon"],"gloss":"someone extremely diligent or skillful; \"he worked like a demon to finish the job on time\"; \"she's a demon at math\""},"10002644":{"pos":"n","lemmas":["demoniac"],"gloss":"someone who acts as if possessed by a demon"},"10002760":{"pos":"n","lemmas":["demonstrator","protester"],"gloss":"someone who participates in a public display of group feeling"},"10002982":{"pos":"n","lemmas":["demonstrator","sales_demonstrator"],"gloss":"someone who demonstrates an article to a prospective buyer"},"10003120":{"pos":"n","lemmas":["demonstrator"],"gloss":"a teacher or teacher's assistant who demonstrates the principles that are being taught"},"10003283":{"pos":"n","lemmas":["denier"],"gloss":"one who denies"},"10003476":{"pos":"n","lemmas":["den_mother"],"gloss":"a woman who supervises a den of Cub Scouts"},"10003575":{"pos":"n","lemmas":["den_mother"],"gloss":"someone who plays the role of a den mother; \"he serves as den mother to all the freshmen in this dormitory\"; \"she's the den mother to new secretaries\""},"10003782":{"pos":"n","lemmas":["dental_assistant"],"gloss":"an assistant to a dentist"},"10003870":{"pos":"n","lemmas":["dental_hygienist"],"gloss":"someone trained to provide preventive dental service (cleaning teeth or taking x-rays)"},"10004019":{"pos":"n","lemmas":["dental_technician","denturist"],"gloss":"someone who makes dental appliances (bridges and dentures)"},"10004171":{"pos":"n","lemmas":["dental_surgeon"],"gloss":"a dentist qualified to perform surgical procedures"},"10004282":{"pos":"n","lemmas":["dentist","tooth_doctor","dental_practitioner"],"gloss":"a person qualified to practice dentistry"},"10004539":{"pos":"n","lemmas":["departer","leaver","goer"],"gloss":"someone who leaves"},"10004718":{"pos":"n","lemmas":["department_head"],"gloss":"the head of a department"},"10004804":{"pos":"n","lemmas":["dependant","dependent"],"gloss":"a person who relies on another person for support (especially financial support)"},"10005006":{"pos":"n","lemmas":["depositor"],"gloss":"a person who has deposited money in a bank or similar institution"},"10005163":{"pos":"n","lemmas":["depressive"],"gloss":"someone suffering psychological depression"},"10005280":{"pos":"n","lemmas":["deputy","lieutenant"],"gloss":"an assistant with power to act when his superior is absent"},"10005548":{"pos":"n","lemmas":["deputy","deputy_sheriff"],"gloss":"someone authorized to exercise the powers of sheriff in emergencies"},"10005721":{"pos":"n","lemmas":["deputy","surrogate"],"gloss":"a person appointed to represent or act on behalf of others"},"10005934":{"pos":"n","lemmas":["deputy"],"gloss":"a member of the lower chamber of a legislative assembly (such as in France)"},"10006081":{"pos":"n","lemmas":["derelict"],"gloss":"a person without a home, job, or property"},"10006177":{"pos":"n","lemmas":["dermatologist","skin_doctor"],"gloss":"a doctor who specializes in the physiology and pathology of the skin"},"10006337":{"pos":"n","lemmas":["dervish"],"gloLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":1,"part":"verb","synsets":{"00001740":{"pos":"v","lemmas":["breathe","take_a_breath","respire","suspire"],"gloss":"draw air into, and expel out of, the lungs; \"I can breathe better when the air is clean\"; \"The patient is respiring\""},"00002325":{"pos":"v","lemmas":["respire"],"gloss":"undergo the biomedical and metabolic processes of respiration by taking up oxygen and producing carbon monoxide"},"00002573":{"pos":"v","lemmas":["respire"],"gloss":"breathe easily again, as after exertion or anxiety"},"00002724":{"pos":"v","lemmas":["choke"],"gloss":"breathe with great difficulty, as when experiencing a strong emotion; \"She choked with emotion when she spoke about her deceased husband\""},"00002942":{"pos":"v","lemmas":["hyperventilate"],"gloss":"breathe excessively hard and fast; \"The mountain climber started to hyperventilate\""},"00003133":{"pos":"v","lemmas":["hyperventilate"],"gloss":"produce hyperventilation in; \"The nurses had to hyperventilate the patient\""},"00003316":{"pos":"v","lemmas":["aspirate"],"gloss":"suck in (air)"},"00003431":{"pos":"v","lemmas":["burp","bubble","belch","eruct"],"gloss":"expel gas from the stomach; \"In China it is polite to burp at the table\""},"00003662":{"pos":"v","lemmas":["force_out"],"gloss":"emit or cause to move with force of effort; \"force out the air\"; \"force out the splinter\""},"00003826":{"pos":"v","lemmas":["hiccup","hiccough"],"gloss":"breathe spasmodically, and make a sound; \"When you have to hiccup, drink a glass of cold water\""},"00004032":{"pos":"v","lemmas":["sigh","suspire"],"gloss":"heave or utter a sigh; breathe deeply and heavily; \"She sighed sadly\""},"00004227":{"pos":"v","lemmas":["exhale","expire","breathe_out"],"gloss":"expel air; \"Exhale when you lift the weight\""},"00004492":{"pos":"v","lemmas":["hold"],"gloss":"keep from exhaling or expelling; \"hold your breath\""},"00004605":{"pos":"v","lemmas":["exhale","give_forth","emanate"],"gloss":"give out (breath or an odor); \"The chimney exhales a thick smoke\""},"00004819":{"pos":"v","lemmas":["sneeze"],"gloss":"exhale spasmodically, as when an irritant entered one's nose; \"Pepper makes me sneeze\""},"00005041":{"pos":"v","lemmas":["inhale","inspire","breathe_in"],"gloss":"draw in (air); \"Inhale deeply\"; \"inhale the fresh mountain air\"; \"The patient has trouble inspiring\"; \"The lung cancer patient cannot inspire air very well\""},"00005526":{"pos":"v","lemmas":["pant","puff","gasp","heave"],"gloss":"breathe noisily, as when one is exhausted; \"The runners reached the finish line, panting heavily\""},"00005815":{"pos":"v","lemmas":["cough"],"gloss":"exhale abruptly, as when one has a chest cold or congestion; \"The smoker coughs all day\""},"00006100":{"pos":"v","lemmas":["hack","whoop"],"gloss":"cough spasmodically; \"The patient with emphysema is hacking all day\""},"00006238":{"pos":"v","lemmas":["expectorate","cough_up","cough_out","spit_up","spit_out"],"gloss":"discharge (phlegm or sputum) from the lungs and out of the mouth"},"00006523":{"pos":"v","lemmas":["snort"],"gloss":"make a snorting sound by exhaling hard; \"The critic snorted contemptuously\""},"00006697":{"pos":"v","lemmas":["wheeze"],"gloss":"breathe with difficulty"},"00006802":{"pos":"v","lemmas":["puff","huff","chuff"],"gloss":"blow hard and loudly; \"he huffed and puffed as he made his way up the mountain\""},"00007012":{"pos":"v","lemmas":["blow"],"gloss":"exhale hard; \"blow on the soup to cool it down\""},"00007193":{"pos":"v","lemmas":["insufflate"],"gloss":"blow or breathe hard on or into"},"00007328":{"pos":"v","lemmas":["yawn"],"gloss":"utter a yawn, as from lack of oxygen or when one is tired; \"The child yawned during the long performance\""},"00007549":{"pos":"v","lemmas":["sniff","sniffle"],"gloss":"inhale audibly through the nose; \"the sick student was sniffling in the back row\""},"00007739":{"pos":"v","lemmas":["blink","wink","nictitate","nictate"],"gloss":"briefly shut the eyes; \"The TV announcer never seems to blink\""},"00008055":{"pos":"v","lemmas":["palpebrate"],"gloss":"wink or blink, especially repeatedly"},"00008195":{"pos":"v","lemmas":["bat","flutter"],"gloss":"wink briefly; \"bat one's eyelids\""},"00008299":{"pos":"v","lemmas":["wink"],"gloss":"signal by winking; \"She winked at him\""},"00008435":{"pos":"v","lemmas":["wink","blink","blink_away"],"gloss":"force to go away by blinking; \"blink away tears\""},"00008602":{"pos":"v","lemmas":["squint","squinch"],"gloss":"cross one's eyes as if in strabismus; \"The children squinted so as to scare each other\""},"00008799":{"pos":"v","lemmas":["squint"],"gloss":"partly close one's eyes, as when hit by direct blinding light; \"The driver squinted as the sun hit his windshield\""},"00008977":{"pos":"v","lemmas":["wince"],"gloss":"make a face indicating disgust or dislike; \"She winced when she heard his pompous speech\""},"00009147":{"pos":"v","lemmas":["shed","molt","exuviate","moult","slough"],"gloss":"cast off hair, skLine truncated
|
||||
@@ -9,10 +9,11 @@
|
||||
return path.startsWith('/chat') ? path : '/chat';
|
||||
};
|
||||
function show(navigated) {
|
||||
if (ready && window.MosaicChat) void window.MosaicChat.render({ path: route(), container: $('chat'), canSend, navigated });
|
||||
if (ready && window.MosaicChat) void window.MosaicChat.render({ path: route(), container: $('chat'), canSend, user: true, navigated });
|
||||
}
|
||||
document.addEventListener('mosaic-signed-out', () => { $('signed-out').hidden = false; $('sign-out').hidden = true; });
|
||||
window.addEventListener('hashchange', () => show(true));
|
||||
setInterval(() => { if (!document.hidden) show(false); }, 5000);
|
||||
fetch('/api/config', { cache: 'no-store', redirect: 'manual' }).then(async res => {
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) { document.dispatchEvent(new Event('mosaic-signed-out')); return; }
|
||||
const config = await res.json();
|
||||
|
||||
@@ -632,7 +632,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
|
||||
const server = createServer(async (req, res) => {
|
||||
// A granted user's request rebinds these below; every other request keeps the owner's.
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false, chatGrants = [];
|
||||
res.setHeader('x-content-type-options', 'nosniff');
|
||||
res.setHeader('referrer-policy', 'no-referrer');
|
||||
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
|
||||
@@ -700,6 +700,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
|
||||
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
|
||||
reads = null; upstream = null; files = USER_FILES; granted = true;
|
||||
chatGrants = sso.chatGrants?.(session) ?? [];
|
||||
}
|
||||
}
|
||||
if (named && preview !== null) {
|
||||
@@ -839,7 +840,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
// `send`: the page shows the composer only where a send can pass (the public name, --send on).
|
||||
// `edit`: the editor shows Save only where a save can pass, the same test as the route's.
|
||||
// `user` and `federation` (FED R6) only when there is one, so the owner's config is unchanged.
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), ...(granted ? { user: true } : {}), ...(hub ? { federation: hub } : {}), send: !tester && Boolean(send) && named, decide: !tester && !granted && Boolean(send) && Boolean(decisionWrite) && named && sso !== null, edit: !tester && !granted && Boolean(send) && Boolean(projectSave) && named });
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), ...(granted ? { user: true, chatGrants } : {}), ...(hub ? { federation: hub } : {}), send: !tester && Boolean(send) && named, decide: !tester && !granted && Boolean(send) && Boolean(decisionWrite) && named && sso !== null, edit: !tester && !granted && Boolean(send) && Boolean(projectSave) && named });
|
||||
if (path === '/healthz') return json(res, 200, { ok: true, ...(sso ? { sso: sso.health() } : {}) });
|
||||
if (path === '/favicon.ico') { res.writeHead(204); return res.end(); }
|
||||
const file = files.get(path);
|
||||
|
||||
@@ -352,6 +352,7 @@ export async function createSso(config, { secrets, fetch: customFetch = null, lo
|
||||
|
||||
return {
|
||||
publicOrigin, publicHost, sessions, logins, handle, session, returnURL,
|
||||
chatGrants: session => users?.chatGrants?.(session) ?? [],
|
||||
health: () => Object.fromEntries([...providers].map(([id, e]) => [id, e.state])),
|
||||
formTargets: () => [...new Set([...providers.values()].filter(e => e.as?.end_session_endpoint).map(e => new URL(e.as.end_session_endpoint).origin))],
|
||||
summary: () => config.providers.map(p => ({ id: p.id, issuer: p.issuer, subjects: p.subjects.length })),
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, readFileSync, writeFileSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { build, parseData, parseIndex, validateSource, EXPECTED, SOURCE } from '../editor-data/build-thesaurus.mjs';
|
||||
import { importWordnet } from '../editor-data/import-wordnet.mjs';
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const hash = b => createHash('sha256').update(b).digest('hex');
|
||||
const scratch = fn => { const d = mkdtempSync(join(tmpdir(), 'thesaurus-')); try { return fn(d); } finally { rmSync(d, { recursive: true, force: true }); } };
|
||||
|
||||
test('pinned source keeps all original files and Princeton license bytes', () => {
|
||||
const m = validateSource();
|
||||
assert.equal(m.archive.sha256, 'cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59');
|
||||
assert.equal(m.archive.bytes, 10775600);
|
||||
assert.equal(Object.keys(m.files).length, 18);
|
||||
assert.equal(m.uncompressedBytes, 36353991);
|
||||
assert.equal(m.lookupBytes, 35435978);
|
||||
assert.deepEqual(readFileSync(join(SOURCE, 'LICENSE')), readFileSync(join(SOURCE, 'wordnet/LICENSE')));
|
||||
assert.match(readFileSync(join(SOURCE, 'LICENSE'), 'utf8'), /WordNet 3.0 Copyright 2006 by Princeton University/);
|
||||
});
|
||||
|
||||
test('two complete conversions match shipped bytes and pinned counts', () => scratch(d => {
|
||||
const a = build({ outDir: join(d, 'a'), buildJson: join(d, 'a-build.json') });
|
||||
const b = build({ outDir: join(d, 'b'), buildJson: join(d, 'b-build.json') });
|
||||
assert.deepEqual(a, b);
|
||||
assert.equal(a.counts.synsets, 117659);
|
||||
assert.equal(a.counts.headwords, 147306);
|
||||
assert.equal(a.counts.literalLemmas, 149229);
|
||||
assert.deepEqual(a.counts, EXPECTED);
|
||||
assert.deepEqual(a, JSON.parse(readFileSync(join(root, 'editor-data/BUILD.json'))));
|
||||
for (const [name, expected] of Object.entries(a.outputs)) {
|
||||
const bytes = readFileSync(join(d, 'a', name));
|
||||
assert.deepEqual(bytes, readFileSync(join(d, 'b', name)));
|
||||
assert.deepEqual(bytes, readFileSync(join(root, 'src/public/thesaurus', name)));
|
||||
assert.equal(hash(bytes), expected.sha256);
|
||||
assert.equal(bytes.length, expected.bytes);
|
||||
}
|
||||
const noun = JSON.parse(readFileSync(join(d, 'a/noun.json')));
|
||||
assert.ok(noun.index.dog.length > 1);
|
||||
assert.ok(noun.index.dog.some(id => noun.synsets[id].lemmas.includes('domestic_dog')));
|
||||
assert.ok(noun.index.dog.every(id => noun.synsets[id].gloss.length > 0));
|
||||
assert.ok(noun.exceptions.children.includes('child'));
|
||||
const adj = JSON.parse(readFileSync(join(d, 'a/adj.json')));
|
||||
assert.ok(Object.values(adj.synsets).some(s => s.pos === 's'));
|
||||
assert.ok(Object.values(adj.synsets).some(s => s.lemmas.some(w => /\((a|p|ip)\)$/.test(w))));
|
||||
}));
|
||||
|
||||
test('changed source, missing license, changed provenance and extra files refuse before output', () => scratch(d => {
|
||||
const sourceRoot = join(d, 'source'); cpSync(SOURCE, sourceRoot, { recursive: true });
|
||||
const data = join(sourceRoot, 'wordnet/data.noun'), bytes = readFileSync(data);
|
||||
writeFileSync(data, Buffer.concat([bytes, Buffer.from('\n')]));
|
||||
assert.throws(() => build({ sourceRoot, outDir: join(d, 'out') }), /integrity/);
|
||||
assert.ok(!readdirSync(d).includes('out'));
|
||||
writeFileSync(data, bytes);
|
||||
rmSync(join(sourceRoot, 'LICENSE'));
|
||||
assert.throws(() => validateSource(sourceRoot), /LICENSE/);
|
||||
cpSync(join(SOURCE, 'LICENSE'), join(sourceRoot, 'LICENSE'));
|
||||
const provenance = join(sourceRoot, 'PROVENANCE.json'), original = readFileSync(provenance);
|
||||
writeFileSync(provenance, Buffer.concat([original, Buffer.from(' ')]));
|
||||
assert.throws(() => validateSource(sourceRoot), /provenance integrity/);
|
||||
writeFileSync(provenance, original);
|
||||
writeFileSync(join(sourceRoot, 'wordnet/extra'), 'unexpected');
|
||||
assert.throws(() => validateSource(sourceRoot), /inventory/);
|
||||
}));
|
||||
|
||||
test('data parser refuses malformed words, offsets, pointers, verb frames and trailing fields', () => {
|
||||
const valid = '00000000 00 n 01 dog 0 000 | a dog\n';
|
||||
assert.equal(parseData(valid, 'noun').synsets['00000000'].gloss, 'a dog');
|
||||
for (const bad of [valid.replace('00000000', '00000001'), valid.replace('01 dog', 'zz dog'), valid.replace('dog 0', 'dog z'), valid.replace('000 |', '001 |'), valid.replace('000 |', '000 extra |'), valid.replace(' | a dog', '')]) {
|
||||
assert.throws(() => parseData(bad, 'noun'), /malformed/);
|
||||
}
|
||||
assert.throws(() => parseData('00000000 00 v 01 run 0 000 01 + 99 00 | run\n', 'verb'), /malformed/);
|
||||
});
|
||||
|
||||
test('index parser refuses wrong counts, nonexistent senses and incorrect lemma mapping', () => {
|
||||
const { synsets } = parseData('00000000 00 n 01 dog 0 000 | a dog\n', 'noun');
|
||||
assert.deepEqual(parseIndex('dog n 1 0 1 0 00000000\n', 'noun', synsets).dog, ['00000000']);
|
||||
for (const bad of ['dog n 2 0 2 0 00000000\n', 'dog n 1 0 1 0 99999999\n', 'cat n 1 0 1 0 00000000\n', 'dog v 1 0 1 0 00000000\n']) {
|
||||
assert.throws(() => parseIndex(bad, 'noun', synsets), /malformed/);
|
||||
}
|
||||
});
|
||||
|
||||
test('offline importer refuses incorrect archive bytes before extraction', () => scratch(d => {
|
||||
const archive = join(d, 'bad.zip'); writeFileSync(archive, 'invalid archive');
|
||||
assert.throws(() => importWordnet(archive, join(d, 'out')), /archive integrity/);
|
||||
assert.ok(!readdirSync(d).includes('out'));
|
||||
}));
|
||||
|
||||
test('verified pinned ZIP round-trips to all original text files', { skip: !process.env.THESAURUS_ARCHIVE }, () => scratch(d => {
|
||||
const out = join(d, 'wordnet');
|
||||
assert.deepEqual(importWordnet(process.env.THESAURUS_ARCHIVE, out), { files: 18, bytes: 36353991 });
|
||||
const m = validateSource();
|
||||
for (const name of Object.keys(m.files)) assert.deepEqual(readFileSync(join(out, name)), readFileSync(join(SOURCE, 'wordnet', name)));
|
||||
assert.throws(() => importWordnet(process.env.THESAURUS_ARCHIVE, out), /already exists/);
|
||||
}));
|
||||
@@ -0,0 +1,87 @@
|
||||
// Row 208: isolated real broker + chat HTTP; no config, slot, provider or live host.
|
||||
import { createServer, request } from 'node:http';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { Broker } from '../../bus/src/broker.mjs';
|
||||
import { Store } from '../../bus/src/store.mjs';
|
||||
import { createChat } from '../src/chat.mjs';
|
||||
import { chatHTTP } from '../src/chat-http.mjs';
|
||||
import { consoleAccess } from '../src/access.mjs';
|
||||
import { BusReadError } from '../src/bus.mjs';
|
||||
|
||||
export const mutants = {
|
||||
composer: ["${threadWrites() ? `<form id=\"chat-send-form\"", "${true ? `<form id=\"chat-send-form\""],
|
||||
open: ['${threadWrites() ? `<button class="btn" type="button" id="chat-resume"', '${true ? `<button class="btn" type="button" id="chat-resume"'],
|
||||
share: ["if (!current.canSend || !threadWrites() || current.tester) return '';", "if (!current.canSend || current.tester) return '';"],
|
||||
create: ["if (current.user && (project || !scopeWrites(key))) return '';", "if (current.user && project) return '';"],
|
||||
};
|
||||
export async function sharingFixture(t, { owner = false, mutant = process.env.POE_UI_MUTANT } = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'poe-sharing-'));
|
||||
const store = new Store(dir), calls = [], lines = [];
|
||||
const access = { accessVersion: 1, revision: 1, users: [
|
||||
{ id: 'alice', home: 'local', subjects: [{ provider: 'fake', sub: 'alice-sub' }] },
|
||||
{ id: 'bob', home: 'local', subjects: [{ provider: 'fake', sub: 'bob-sub' }] },
|
||||
], grants: ['alice', 'bob'].map(user => ({ id: 'g-' + user, user, business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator' })) };
|
||||
const broker = new Broker({ store, access: () => access, businesses: { demo: {
|
||||
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
|
||||
roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } }, assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } } },
|
||||
definitions: { 'chief-of-staff': {}, assistant: { messaging: JSON.parse(readFileSync(new URL('../../../roles/assistant.json', import.meta.url))).messaging } },
|
||||
} } });
|
||||
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const invoke = (cap, verb, args = {}) => broker.request(cap, { verb, args });
|
||||
const alice = () => broker.bindUser({ business: 'demo', user: 'alice' });
|
||||
const bob = () => broker.bindUser({ business: 'demo', user: 'bob' });
|
||||
const project = invoke(human, 'project.create', { name: 'Viewer project' }).id;
|
||||
const other = invoke(human, 'project.create', { name: 'Collaborator project' }).id;
|
||||
const own = invoke(alice(), 'thread.create', { project, instance: 'assistant', name: 'Old own thread' }).id;
|
||||
const writable = invoke(alice(), 'thread.create', { project: other, instance: 'assistant', name: 'Writable thread' }).id;
|
||||
const shared = invoke(bob(), 'thread.create', { project, instance: 'assistant', name: 'Shared thread <img src=x onerror=alert(1)>' }).id;
|
||||
invoke(alice(), 'thread.send', { thread: own, body: 'Own history' });
|
||||
invoke(bob(), 'thread.send', { thread: shared, body: 'Shared history' });
|
||||
invoke(bob(), 'thread.share', { thread: shared, to: 'user:alice' });
|
||||
access.grants[0].level = 'viewer';
|
||||
const hints = consoleAccess({ load: () => access, business: () => 'demo' });
|
||||
const principal = () => owner ? human : alice();
|
||||
const chat = createChat({ call: async (verb, args) => {
|
||||
calls.push([verb, args]);
|
||||
if (verb === 'plugins') return { plugins: { channel: ['webui'] } };
|
||||
try { return invoke(principal(), verb, args); }
|
||||
catch (e) { throw new BusReadError(e.code); }
|
||||
} });
|
||||
const source = readFileSync(new URL('../src/public/chat.js', import.meta.url), 'utf8');
|
||||
let chatSource = source;
|
||||
if (mutant) {
|
||||
const [before, after] = mutants[mutant] ?? [];
|
||||
if (!before || !source.includes(before)) throw Error('mutant anchor missing: ' + mutant);
|
||||
chatSource = source.replace(before, after);
|
||||
}
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://fixture');
|
||||
if (url.pathname === '/api/config') {
|
||||
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
||||
return res.end(JSON.stringify({ send: true, user: !owner, chatGrants: hints.chatGrants({ user: 'alice', provider: 'fake', sub: 'alice-sub' }) }));
|
||||
}
|
||||
if (url.pathname.startsWith('/api/chat/')) return chatHTTP(req, res, {
|
||||
path: url.pathname, search: url.search, chat, named: true, send: true,
|
||||
preview: { sso: true, user: owner ? 'jason' : 'alice', origin: origin() }, log: line => lines.push(line),
|
||||
});
|
||||
if (url.pathname === '/chat.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(chatSource); }
|
||||
if (url.pathname === '/') {
|
||||
res.writeHead(200, { 'content-type': 'text/html' });
|
||||
return res.end('<!doctype html><title>Sharing fixture</title><div id="view"></div><script src="/chat.js"></script>');
|
||||
}
|
||||
res.writeHead(404); res.end();
|
||||
});
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||
const origin = () => `http://127.0.0.1:${server.address().port}`;
|
||||
t.after(async () => { await new Promise(r => server.close(r)); store.close(); rmSync(dir, { recursive: true, force: true }); });
|
||||
const http = (path, value, extra = {}) => new Promise((resolve, reject) => {
|
||||
const body = value === undefined ? undefined : JSON.stringify(value);
|
||||
const req = request(origin() + path, { method: body ? 'POST' : 'GET', headers: body ? { origin: origin(), 'sec-fetch-site': 'same-origin', 'content-type': 'application/json', ...extra } : extra }, res => {
|
||||
let data = ''; res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data) }));
|
||||
}); req.on('error', reject); req.end(body);
|
||||
});
|
||||
return { store, access, broker, human, alice, bob, invoke, project, other, own, writable, shared, calls, lines, origin: origin(), http, hints, source, chatSource };
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { sharingFixture } from './thread-sharing-fixture.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
import { startServer } from '../src/serve.mjs';
|
||||
import { consoleBus } from '../src/console-bus.mjs';
|
||||
|
||||
async function page(t, f) {
|
||||
const b = await browser(); t.after(() => b.close());
|
||||
await b.navigate(f.origin);
|
||||
const render = async path => {
|
||||
await b.evaluate(`(async()=>{location.hash=${JSON.stringify('#' + path)};await MosaicChat.render({path:${JSON.stringify(path)},container:document.getElementById('view'),canSend:true,user:${!f.owner},navigated:true});return true;})()`);
|
||||
};
|
||||
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}return false;})()`);
|
||||
return { b, render, wait };
|
||||
}
|
||||
const counts = f => Object.fromEntries(['threads', 'messages', 'thread_opens', 'thread_shares'].map(table => [table, f.store.get(`SELECT count(*) n FROM ${table}`).n]));
|
||||
|
||||
test('193d HTTP shares use the caller, plain/shared lists, exact args and write fences', async t => {
|
||||
const f = await sharingFixture(t);
|
||||
let r = await f.http('/api/chat/threads?shared=true');
|
||||
assert.equal(r.status, 200); assert.deepEqual(r.body.rows.map(x => x.id), [f.shared]);
|
||||
assert.equal(r.body.rows[0].owner, 'user:bob');
|
||||
r = await f.http('/api/chat/threads');
|
||||
assert.deepEqual(r.body.rows.map(x => x.id), [f.own, f.writable]);
|
||||
assert.ok(r.body.rows.every(x => !Object.hasOwn(x, 'owner')), 'plain list shape unchanged');
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.shared}/messages`)).status, 200);
|
||||
const before = counts(f);
|
||||
for (const [path, body] of [
|
||||
['/api/chat/threads', { project: f.project, instance: 'assistant', name: 'forbidden' }],
|
||||
[`/api/chat/threads/${f.own}/messages`, { body: 'forbidden' }],
|
||||
[`/api/chat/threads/${f.own}/open`, {}],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob' }],
|
||||
[`/api/chat/threads/${f.own}/unshare`, { to: 'user:bob' }],
|
||||
]) {
|
||||
r = await f.http(path, body);
|
||||
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'access-refused'); assert.equal(r.body.sent, false);
|
||||
}
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/shares`)).body.error, 'access-refused');
|
||||
assert.deepEqual(counts(f), before);
|
||||
f.access.grants[0].level = 'collaborator';
|
||||
r = await f.http(`/api/chat/threads/${f.own}/share`, { to: 'user:bob' });
|
||||
assert.equal(r.status, 200); assert.deepEqual(r.body, { thread: f.own, to: 'user:bob', op: 'share' });
|
||||
assert.deepEqual((await f.http(`/api/chat/threads/${f.own}/shares`)).body.rows.map(x => x.to), ['user:bob']);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/unshare`, { to: 'user:bob' })).status, 200);
|
||||
for (const [path, body] of [
|
||||
['/api/chat/threads?shared=false', undefined], ['/api/chat/threads?shared=true&shared=true', undefined],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob', owner: 'jason' }],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob', verb: 'thread.send' }],
|
||||
[`/api/chat/threads/${f.own}/shares?to=user:bob`, undefined],
|
||||
]) assert.equal((await f.http(path, body)).status, 400, path);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/share`, { to: '*' }, { origin: 'http://other.test' })).status, 403);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/share`, { to: '*' }, { 'sec-fetch-site': 'cross-site' })).status, 403);
|
||||
f.invoke(f.bob(), 'thread.unshare', { thread: f.shared, to: 'user:alice' });
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.shared}/messages`)).body.error, 'thread-not-owned');
|
||||
});
|
||||
|
||||
test('193d config supplies only the signed-in user live collaborator scopes and updates on downgrade', async t => {
|
||||
const f = await sharingFixture(t);
|
||||
f.access.grants.push({ ...f.access.grants[0], id: 'mixed', level: 'collaborator', projects: [f.other] });
|
||||
const session = { user: 'alice', name: 'alice', provider: 'fake', sub: 'alice-sub' };
|
||||
let web;
|
||||
const sso = { get publicHost() { return `127.0.0.1:${web.address().port}`; }, get publicOrigin() { return `http://${this.publicHost}`; },
|
||||
formTargets: () => [], session: () => session, chatGrants: s => f.hints.chatGrants(s) };
|
||||
web = await startServer({ port: 0, sso, log: () => {}, userOptions: () => consoleBus(async () => [], { send: true }) });
|
||||
t.after(() => new Promise(r => web.close(r)));
|
||||
const read = async () => (await fetch(`http://127.0.0.1:${web.address().port}/api/config`)).json();
|
||||
let config = await read();
|
||||
assert.equal(config.user, true); assert.deepEqual(config.chatGrants, [{ projects: [f.other], instances: ['assistant'] }]);
|
||||
assert.equal(config.send, true, 'send mode is separate from individual grant levels');
|
||||
f.access.grants[2].level = 'viewer';
|
||||
assert.deepEqual((await read()).chatGrants, []);
|
||||
f.access.grants[2].level = 'collaborator'; f.access.grants[2].expires = new Date(Date.now() - 1000).toISOString();
|
||||
assert.deepEqual((await read()).chatGrants, []);
|
||||
assert.deepEqual(f.hints.chatGrants({ ...session, sub: 'bob-sub' }), []);
|
||||
});
|
||||
|
||||
test('193d viewer UI has no composer, resume, share, session mutations or create controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat');
|
||||
assert.match(await b.evaluate('document.getElementById("view").textContent'), /Shared with me/);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-project-form")'), false);
|
||||
await render('/chat/projects/' + f.project);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-thread-form")'), false);
|
||||
for (const thread of [f.own, f.shared]) {
|
||||
await render('/chat/threads/' + thread);
|
||||
assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /history/);
|
||||
for (const selector of ['#chat-send-form', '#chat-resume', '#chat-share-form', '#chat-session-form', '#chat-clear', '#chat-cancel']) {
|
||||
assert.equal(await b.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`), false, `${thread}: ${selector}`);
|
||||
}
|
||||
}
|
||||
assert.equal(await b.evaluate('!!document.querySelector("img")'), false, 'shared name remains text');
|
||||
const sharedSessions = f.calls.filter(([verb, args]) => verb === 'session.list' && args.thread === f.shared);
|
||||
assert.equal(sharedSessions.length, 0, 'shared read never requests an owner-only catalog');
|
||||
});
|
||||
|
||||
test('193d mixed grants keep viewer project read-only and collaborator own thread writable; downgrade hides controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
f.access.grants.push({ ...f.access.grants[0], id: 'mixed', level: 'collaborator', projects: [f.other] });
|
||||
const { b, render, wait } = await page(t, f);
|
||||
await render('/chat/threads/' + f.own);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'), false);
|
||||
await render('/chat/threads/' + f.writable);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'), true);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form")'), true);
|
||||
await b.evaluate('(()=>{const x=document.getElementById("chat-share-target");x.value="user:bob";x.dispatchEvent(new Event("input",{bubbles:true}));document.querySelector("#chat-share-form button").click();return true;})()');
|
||||
assert.equal(await wait('!!document.querySelector("[data-unshare]")'), true);
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM thread_shares WHERE thread=? AND op='share'", f.writable).n, 1);
|
||||
await b.evaluate('document.querySelector("[data-unshare]").click(),true');
|
||||
assert.equal(await wait('!document.querySelector("[data-unshare]")'), true);
|
||||
f.access.grants[2].level = 'viewer';
|
||||
await render('/chat/threads/' + f.writable);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form,#chat-share-form,#chat-resume")'), false);
|
||||
// Loss of a share clears visible history on the next render.
|
||||
await render('/chat/threads/' + f.shared);
|
||||
f.invoke(f.bob(), 'thread.unshare', { thread: f.shared, to: 'user:alice' });
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.equal(await b.evaluate('!!document.querySelector(".chat-log")'), false);
|
||||
});
|
||||
|
||||
test('193d owner has share controls on own threads and shared threads remain read-only', async t => {
|
||||
const f = await sharingFixture(t, { owner: true }); f.owner = true;
|
||||
const own = f.invoke(f.human, 'thread.create', { project: f.project, instance: 'assistant', name: 'Owner thread' }).id;
|
||||
f.invoke(f.bob(), 'thread.share', { thread: f.shared, to: 'jason' });
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + own);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form")'), true);
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /Shared history/);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form,#chat-send-form,#chat-resume")'), false);
|
||||
});
|
||||
|
||||
// Run under each served-source mutant after the normal absence test kills it.
|
||||
// Every browser POST bypasses all local UI checks and still uses the viewer transport.
|
||||
test('193d bypassed UI checks still reach a refusing bus for every viewer write', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + f.own);
|
||||
const before = counts(f);
|
||||
for (const [path, value] of [
|
||||
['threads', { project: f.project, instance: 'assistant', name: 'mutant create' }],
|
||||
[`threads/${f.own}/messages`, { body: 'mutant send' }],
|
||||
[`threads/${f.own}/open`, {}], [`threads/${f.own}/share`, { to: 'user:bob' }],
|
||||
]) {
|
||||
const r = JSON.parse(await b.evaluate(`(async()=>{const r=await fetch('/api/chat/'+${JSON.stringify(path)},{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(${JSON.stringify(value)})});return JSON.stringify({status:r.status,body:await r.json()});})()`));
|
||||
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'access-refused'); assert.equal(r.body.sent, false);
|
||||
}
|
||||
assert.deepEqual(counts(f), before);
|
||||
for (const verb of ['thread.create', 'thread.send', 'thread.open', 'thread.share']) assert.ok(f.calls.some(([v]) => v === verb), `${verb} reached the bus`);
|
||||
});
|
||||
|
||||
|
||||
test('193d collaborator shares stay read-only and never expose reshare or wake controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
f.access.grants[0].level = 'collaborator';
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form,#chat-send-form,#chat-resume,#chat-session-form")'), false);
|
||||
for (const [action, body] of [['messages', { body: 'shared write' }], ['open', {}], ['share', { to: '*' }]]) {
|
||||
const r = await f.http(`/api/chat/threads/${f.shared}/${action}`, body);
|
||||
assert.equal(r.status, 403); assert.equal(r.body.error, 'thread-not-owned');
|
||||
}
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
WordNet Release 3.0
|
||||
|
||||
This software and database is being provided to you, the LICENSEE, by
|
||||
Princeton University under the following license. By obtaining, using
|
||||
and/or copying this software and database, you agree that you have
|
||||
read, understood, and will comply with these terms and conditions.:
|
||||
|
||||
Permission to use, copy, modify and distribute this software and
|
||||
database and its documentation for any purpose and without fee or
|
||||
royalty is hereby granted, provided that you agree to comply with
|
||||
the following copyright notice and statements, including the disclaimer,
|
||||
and that the same appear on ALL copies of the software, database and
|
||||
documentation, including modifications that you make for internal
|
||||
use or for distribution.
|
||||
|
||||
WordNet 3.0 Copyright 2006 by Princeton University. All rights reserved.
|
||||
|
||||
THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON
|
||||
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PRINCETON
|
||||
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES OF MERCHANT-
|
||||
ABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE
|
||||
OF THE LICENSED SOFTWARE, DATABASE OR DOCUMENTATION WILL NOT
|
||||
INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR
|
||||
OTHER RIGHTS.
|
||||
|
||||
The name of Princeton University or Princeton may not be used in
|
||||
advertising or publicity pertaining to distribution of the software
|
||||
and/or database. Title to copyright in this software, database and
|
||||
any associated documentation shall at all times remain with
|
||||
Princeton University and LICENSEE agrees to preserve same.
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user