diff --git a/.agents/skills/mex-inbox/.mex-managed.json b/.agents/skills/mex-inbox/.mex-managed.json index 3ed7807c..60a0e41e 100644 --- a/.agents/skills/mex-inbox/.mex-managed.json +++ b/.agents/skills/mex-inbox/.mex-managed.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "owner": "mex-agent", "skill": "mex-inbox", - "packageVersion": "0.8.0", + "packageVersion": "0.8.1", "files": { - "SKILL.md": "ec0e61b45c9f2e2d70074bd613c0d294dd4d88ec73964e6187f0ac8d51de1505", - "agents/openai.yaml": "4dbdf205a76ba17d9fd8984c8d546b1d6628f0e135bbaf0c16c773f8fa6fc765", - "references/cli-workflows.md": "5147aa68c8fea7a96dd5c569c23570238e2a32a46655fd14267e31a7c5d12c30" + "SKILL.md": "39cf595ac48203bf49ea2e273ea1c1c25e7ebbf4616c68596cd8e1c08eaf2738", + "agents/openai.yaml": "12a2827fe257c303d926122e4bb590910abd660e6451550a1db68a48037b4402", + "references/cli-workflows.md": "6c91905cc69d8ea47080630ee1551bf1a61ec33605fff01463d4a2ad71f497c6" } } diff --git a/.agents/skills/mex-inbox/SKILL.md b/.agents/skills/mex-inbox/SKILL.md index 4f6e5409..7ddb707c 100644 --- a/.agents/skills/mex-inbox/SKILL.md +++ b/.agents/skills/mex-inbox/SKILL.md @@ -1,24 +1,24 @@ --- name: mex-inbox -description: Prepare and manage governed MEX Inbox proposals for durable Spec-family knowledge. Use when the user clearly asks to capture, create, save, or draft a MEX product Spec, requirement, constraint, acceptance criterion, or durable product-spec decision for team review, or explicitly invokes /mex-inbox or $mex-inbox. Do not use for email inboxes, generic notes, vague brainstorming, OpenAPI or test specs mentioned only by the word "spec," architecture pages, conventions, patterns, Workstreams, Relays, arbitrary Wiki pages, or generic memory. +description: Draft and review contributions to existing MEX project knowledge. Use when the user asks to capture a discussion or decision in project knowledge, propose an addition or correction for team review, or explicitly invokes /mex-inbox or $mex-inbox. Also supports existing Spec proposals. Do not activate for brainstorming alone, routine GROW upkeep, email inboxes, session logs, or handoffs. --- # MEX Inbox -Prepare governed Spec-memory proposals. Treat Inbox as a proposal workflow, not email and not a general Wiki mutation system. +Turn an explicit request to retain project knowledge into a focused contribution for review. Inbox proposals are review artifacts; accepted knowledge belongs in the existing Wiki Markdown. Ordinary GROW upkeep can continue directly. ## Keep the scope honest -- Create exactly one `spec.create` or `spec.update` change per draft. -- Support only `spec`, `requirement`, `constraint`, and `acceptance_criterion` entities. -- Reject unsupported durable knowledge honestly. Do not silently translate architecture, conventions, patterns, Workstreams, Relays, or arbitrary Wiki pages into Specs. -- Require a clear durable claim. Do not capture unresolved brainstorming as team memory. +- Create one `knowledge.create` or `knowledge.update` change per draft, for an existing kind: `architecture`, `component`, `convention`, `decision`, `pattern`, or `guide`. +- Existing Spec workflows also support `spec.create` and `spec.update` for `spec`, `requirement`, `constraint`, and `acceptance_criterion`. Choose these only for actual Spec-family intent. +- Capture durable conclusions, their rationale, and useful evidence. Do not dump the conversation or present unresolved ideas as agreed facts. +- Do not route session logs, Relays, or routine GROW edits through Inbox merely because they contain context. ## Prepare a draft -1. Distill the durable knowledge, rationale, and useful evidence from the conversation. -2. Decide whether it creates a new Spec-family entity or updates an existing exact entity. -3. For an update, resolve the canonical target and current revisions before drafting. Never guess an ID or revision. +1. Distill what future agents or teammates need to know from the user's request and discussion. +2. Search existing knowledge first. Prefer correcting or extending the relevant record or section when it already covers the subject; create a new entry when the claim has no suitable home. +3. Read the exact target and current revisions before drafting a correction. Preserve unrelated knowledge when replacing its body. Never guess an ID or revision. 4. Preserve useful files, code, commits, entities, and external links as evidence only when actually available. Never invent provenance, authors, dates, events, or history. 5. Build one request against the action-scoped runtime contract. 6. Run the exact preview flow and summarize the human effect before technical details. @@ -41,4 +41,4 @@ Read [references/cli-workflows.md](references/cli-workflows.md) before executing - Lead with what the proposal means to a person. Do not expose envelopes, hashes, revision machinery, or raw diffs unless diagnosis requires them. - After a local draft write, say exactly what changed and that it remains checkout-only. - After a canonical write, say which Git-tracked MEX artifacts were written to the working tree and that commit/push is still required to share them. Never claim that MEX committed or pushed. -- When MEX context materially affects the answer, add one concise acknowledgement: `MEX context used: .` +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. diff --git a/.agents/skills/mex-inbox/agents/openai.yaml b/.agents/skills/mex-inbox/agents/openai.yaml index 9e0ac8cf..de507237 100644 --- a/.agents/skills/mex-inbox/agents/openai.yaml +++ b/.agents/skills/mex-inbox/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "MEX Inbox" - short_description: "Draft governed MEX Spec proposals" - default_prompt: "Use $mex-inbox to draft this durable requirement for MEX team review." + short_description: "Propose additions to project knowledge" + default_prompt: "Use $mex-inbox to capture what we decided as a contribution to this project's knowledge." diff --git a/.agents/skills/mex-inbox/references/cli-workflows.md b/.agents/skills/mex-inbox/references/cli-workflows.md index 76e79582..c0f9215d 100644 --- a/.agents/skills/mex-inbox/references/cli-workflows.md +++ b/.agents/skills/mex-inbox/references/cli-workflows.md @@ -23,7 +23,17 @@ Use one of these command IDs: Treat this bounded action result as the exact runtime source for the request shape, available examples, constraints, preview command, and apply command. Do not dump `mex capabilities --json` or the full Inbox contract during ordinary execution. Write request and preview JSON only to ordinary regular files inside the checkout or an approved temporary directory; do not use symlinks. -## Resolve a Spec update +## Find existing knowledge and resolve a correction + +1. Use `mex wiki query --limit 10 --json` or `mex wiki list --type --limit 25 --json` to find related records before creating another. +2. Read the best candidate with `mex inbox target --json`. This bounded read returns `target`, `version`, `sourcePath`, and the existing body from the current Wiki index, without initializing or repairing it. +3. For `knowledge.update`, copy `target` and use `version.contentHash` as the target revision and `version.semanticRevision` as the semantic revision. Select a section entity when only that section should change. The patch body replaces the target's body; preserve its relevant existing content. A target can fit the read response but exceed the 16 KiB update-body limit; choose a smaller section or a title/summary-only patch when appropriate, never truncate existing knowledge to fit. +4. If no suitable record exists, use `knowledge.create` with the appropriate existing kind. MEX chooses a path in `context/` or `patterns/`; do not invent an Inbox knowledge category or supply an arbitrary destination path. +5. If several targets remain plausible after reading them, ask which one the user intends. If a read reports a stale or unavailable index, address its explicit maintenance requirement before relying on it; never fabricate revisions or silently refresh an index as part of a read. + +For create-time topics, find them with Wiki reads, then resolve each with `mex inbox target --json` for current revisions. Topics can be dependencies but are not Inbox correction targets. Omit topics when none are needed; normal knowledge creates have no relation editor. + +## Resolve a legacy Spec update 1. Use `mex spec list --json` to identify candidates. 2. Use `mex spec show --json` for the exact candidate. @@ -36,7 +46,7 @@ Do not invent target IDs, relation endpoints, topic IDs, or revisions. For a cre ## Save a checkout-local draft 1. Resolve `inbox.draft.save`. -2. Create a unique operation ID and a request containing one `spec.create` or `spec.update` draft. +2. Create a unique operation ID and a request containing one `knowledge.create` or `knowledge.update` draft (or a Spec change for actual Spec-family intent). 3. For a new draft, provide no unrelated expectations. For an existing draft update, read it with `mex inbox draft show --json` and use its exact current local revision. 4. Preview with `mex inbox draft save --json` and capture the complete successful JSON wrapper unchanged. Require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. 5. Summarize the proposed local effect. If the user asked to create/save/draft, apply with `mex inbox draft save --apply --json` without another confirmation. @@ -60,7 +70,7 @@ The apply writes only checkout-local draft state. It does not create a canonical 5. Apply the exact preview with `mex inbox publish --apply --json`. 6. Return `/inbox?view=review&proposal=`. -Publishing removes the exact local draft after creating the pending proposal. It does not approve the proposal or write the requested Spec change. +Publishing removes the exact local draft after creating the pending proposal. It does not approve the proposal or change accepted knowledge. The proposal is Markdown in the working tree; teammates receive it through Git. ## Review canonical proposals @@ -73,4 +83,4 @@ Use `mex inbox proposal list --json` and `mex inbox proposal show 5. Wait for fresh explicit confirmation. 6. Apply the exact preview with the same command plus `--apply --json`. -Approval writes the proposed Spec-family entity change, proposal decision, Wiki ledger, and Activity records to the working tree. Reject and withdraw make a terminal proposal decision without changing the Spec. Mark stale changes a pending proposal to stale only when MEX proves dependency drift. Repair replaces stale intent, clears prior review, and returns the proposal to pending without changing the Spec. These canonical transitions write Activity records; none commits, pushes, pulls, stages, or notifies teammates. +Approval writes the proposed knowledge change, proposal decision, Wiki ledger, and Activity records to the working tree. The proposal remains review history. Reject and withdraw make a terminal proposal decision without changing knowledge. Mark stale changes a pending proposal to stale only when MEX proves dependency drift. Repair replaces stale intent, clears prior review, and returns the proposal to pending without changing knowledge. These canonical transitions write Activity records; none commits, pushes, pulls, stages, or notifies teammates. diff --git a/.agents/skills/mex-relay/.mex-managed.json b/.agents/skills/mex-relay/.mex-managed.json index 5f6e777e..f1eda046 100644 --- a/.agents/skills/mex-relay/.mex-managed.json +++ b/.agents/skills/mex-relay/.mex-managed.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "owner": "mex-agent", "skill": "mex-relay", - "packageVersion": "0.8.0", + "packageVersion": "0.8.1", "files": { - "SKILL.md": "70fa6eb782225f6357b0046ea9449e80095e6c01bc5e960886a2f60fad7e3f25", + "SKILL.md": "186d1d19c4b513f4b98ee27bf0259744d1ef79ffc758676484596cfe498aa502", "agents/openai.yaml": "9c2e9c2e34d60ec0f4d07089b07d9e49a6c8210c2a3e277593c15a2635f03076", - "references/cli-workflows.md": "98be305e6d941acdd5caa7697317c963343bb62ddcc25af8e129d0af1ea0aaa8" + "references/cli-workflows.md": "56b6e74129cdc6ddb5121480b03e01fd2f1f65b7b8c0b7899552a4a217d48cf5" } } diff --git a/.agents/skills/mex-relay/SKILL.md b/.agents/skills/mex-relay/SKILL.md index a1e76f61..15935610 100644 --- a/.agents/skills/mex-relay/SKILL.md +++ b/.agents/skills/mex-relay/SKILL.md @@ -10,12 +10,12 @@ Prepare durable team handoffs that another engineer can continue from. Never rep ## Prepare a Relay draft 1. Infer the useful session state: a concise summary, current position, completed work, in-progress work, blockers, unresolved questions, next actions, and relevant decisions, files, code, commits, or external links. -2. Resolve intended recipients against active MEX Members. Never fabricate member IDs. -3. Default to a standalone Relay. -4. Include a Workstream only when a real, relevant Workstream already exists. Preserve it as supported typed context; never invent one or turn Relay creation into Workstream creation. +2. For “whoever picks this up,” choose open-to-team. It includes future active project Members; no Member lookup is needed to save this local draft. If the user names people, retain named-recipient intent and resolve exact IDs only when needed. An unresolved named draft may stay recipient-free locally; do not silently publish it to everyone. +3. Default to a standalone Relay. Use the local-save shortcut for a new draft; keep the structured preview/apply path for exact updates and canonical actions. +4. Include an existing relevant Workstream only as typed evidence. Never invent one or turn saving a handoff into Workstream creation. 5. Add optional typed context references only when the referenced IDs, paths, commits, or URLs are known. Never invent provenance. -6. Build one request against the action-scoped runtime contract and run the exact preview. -7. When the user already asked to create, save, or draft the handoff, apply that exact checkout-local draft preview without another confirmation. +6. Resolve the action-scoped runtime contract. For a new draft, `mex relay draft save --from --json` performs the exact local preview/apply internally. For an update, build and preview the structured request. +7. An explicit create/save/draft request authorizes that local write without another confirmation. Publication remains separate. 8. Return `/relays?view=drafts&draft=` and state that the draft is checkout-local and nothing has been delivered or shared. Read [references/cli-workflows.md](references/cli-workflows.md) before executing any Relay mutation. Load only the operation being performed. @@ -31,8 +31,9 @@ Read [references/cli-workflows.md](references/cli-workflows.md) before executing ## Preserve lifecycle meaning -- Treat taking as the explicit canonical acknowledge action by an intended recipient. +- Taking records one claimant: a named active recipient, or any active project Member for an open-to-team Relay. Future Members qualify after joining and receiving the artifact through Git. Separate offline claims can still require Git conflict resolution. +- An inactive Member can be reactivated through the existing Team identity workflow, preserving their ID and older handoffs. Preview and confirm this canonical change separately; never change identity merely to bypass Relay eligibility. - Treat closing as “this handoff no longer needs attention.” Do not claim it completes a linked task, issue, pull request, or Workstream. - After publication, say that Git-tracked Relay and Activity records were written to the working tree. Explain that teammates receive them only after commit/push and their own pull or refresh. - Never claim that MEX sent a notification, committed, pushed, pulled, staged, assigned work, or completed another system's object. -- When MEX context materially affects the answer, add one concise acknowledgement: `MEX context used: .` +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. diff --git a/.agents/skills/mex-relay/references/cli-workflows.md b/.agents/skills/mex-relay/references/cli-workflows.md index 5d9b481d..89f81227 100644 --- a/.agents/skills/mex-relay/references/cli-workflows.md +++ b/.agents/skills/mex-relay/references/cli-workflows.md @@ -20,11 +20,11 @@ Use one of these command IDs: Treat this bounded action result as the exact runtime source for the request shape, available examples, constraints, preview command, and apply command. Do not dump `mex capabilities --json` or the full Relay contract during ordinary execution. Write request and preview JSON only to ordinary regular files inside the checkout or an approved temporary directory; do not use symlinks. -## Resolve recipients and optional context +## Choose an audience and optional context -1. Run `mex member list --active --limit 100 --json`, following bounded cursors when present, and match the user's names or team intent to active Members. -2. Use `mex member show --json` when a candidate needs disambiguation or a current artifact revision is required. -3. Stop and ask for recipient clarification when multiple active Members remain plausible, no active Member matches, or bounded results cannot prove uniqueness. Relay stores individual Members, so never fabricate a group for “the backend team.” +1. For an open handoff or “save for whoever picks it up,” use `audience: "team"` and `recipients: []`. Do not enumerate today's Members; active Members who join later must remain eligible. This setting does not share the local draft. +2. For named people, use `audience: "members"`. Resolve names with `mex member list --active --limit 100 --json` and bounded cursors. Local drafts may retain an empty recipient array while names are unresolved; record the intended name in `unresolvedQuestions` and resolve it before publication. +3. Use `mex member show --json` for disambiguation or exact publication revisions. Ask when named recipients remain ambiguous; never reinterpret a named subset such as “the backend team” as every project Member. 4. Default to no Workstream lookup. If the user named a Workstream or the handoff clearly belongs to an existing one, run `mex workstream list --json` and `mex workstream show --json` to resolve it exactly. 5. In Relay v3, preserve a relevant Workstream as typed entity evidence when the selected contract supports it; do not author the legacy top-level Workstream field. @@ -33,12 +33,23 @@ Never fabricate recipient IDs, entity IDs, revisions, commits, code fingerprints ## Save a checkout-local draft 1. Resolve `relay.draft.save`. -2. Create a unique operation ID and a standalone draft with active recipient references and a concise summary. -3. Include only useful non-empty context sections. Prefer accurate omissions over invented completeness. -4. For a new draft, provide no unrelated expectations. For an existing draft update, read it with `mex relay draft show --json` and use its exact local revision. -5. Preview with `mex relay draft save --json` and capture the complete successful JSON wrapper unchanged. Require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. -6. Summarize recipients and continuation state. If the user asked to create/save/draft, apply with `mex relay draft save --apply --json` without another confirmation. -7. Read the returned draft ID and respond with `/relays?view=drafts&draft=`. +2. Write one temporary JSON object containing the draft content. Include a concise summary, the intended audience, and only useful non-empty context sections. Prefer accurate omissions over invented completeness. +3. For a new draft, run `mex relay draft save --from --operation-id --json`. This validates the content, obtains a signed local preview, and applies that exact preview internally. It never publishes and needs no Member resolution. Do not combine `--from` with a request file or `--apply`. +4. Require `ok: true`, `mode: "apply"`, and `data.applied: true`; read the draft ID from `data.localChanges` and return `/relays?view=drafts&draft=`. The shortcut retains the original preview under `.mex/local/relay-previews/` before applying and removes it after success. An interrupted save can resume with the same operation ID and unchanged content, or the exact `problem.recovery` command. Do not replace the receipt or create another operation to work around an interrupted/conflicting result; if recovery is refused, report it and inspect the existing draft state. + +For example, a new open handoff may contain: + +```json +{ + "audience": "team", + "recipients": [], + "summary": "Continue the parser correction.", + "completed": ["Reproduced the failure with the nested-import fixture."], + "nextActions": ["Check whether the same failure affects exported aliases."] +} +``` + +For an existing draft update, read it with `mex relay draft show --json`, preserve its content, and use its exact local revision in the structured request. Preview with `mex relay draft save --json`; require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. Apply the complete captured envelope unchanged with `mex relay draft save --apply --json`. An explicit save/update request needs no additional confirmation for this checkout-local write. The apply writes only checkout-local draft state in `.mex/local/team.db`. It does not create a canonical Relay or Activity record, deliver a handoff, commit, push, or notify anyone. Apply before the preview expires; if anything changes or the preview becomes stale, preview again instead of reconstructing it. @@ -51,8 +62,8 @@ The apply writes only checkout-local draft state in `.mex/local/team.db`. It doe ## Publish a Relay -1. Read the exact draft and every active recipient Member required by the selected contract. -2. Resolve `relay.publish`, build exact draft/member expectations, and preview with `mex relay publish --json`. +1. Read the exact draft. For an open-to-team audience, no recipient Member list is needed. For named recipients, require at least one and read each current active Member; an empty named draft must be completed before publication. +2. Resolve `relay.publish`. Team publication expects only the exact local draft revision; named publication also expects every recipient Member revision. Preview with `mex relay publish --json`. The service verifies the current active sender in both cases. 3. Explain that applying replaces the private local draft with canonical Git-tracked Relay and Activity records in the working tree, records the service-observed branch/HEAD/dirty repository state without copying dirty source contents, and does not deliver through a notification service or share before Git commit/push and teammate pull/refresh. 4. Wait for fresh explicit confirmation. 5. Apply the exact preview with `mex relay publish --apply --json`. @@ -60,9 +71,9 @@ The apply writes only checkout-local draft state in `.mex/local/team.db`. It doe ## Take a Relay -1. Resolve the exact published Relay with `mex relay show --json` and verify that the current actor is an intended active recipient. +1. Resolve the exact published Relay with `mex relay show --json`. A team audience allows the current active project Member; a named audience requires the current Member to be listed. An omitted audience on an older Relay means named recipients. 2. Resolve `relay.acknowledge` and preview with `mex relay acknowledge --json`. -3. Explain that applying makes the current recipient the sole claimant, prevents another recipient from taking it, and writes canonical Relay/Activity state in the working tree. There is no unclaim or reassignment action, and it does not assign or start a task elsewhere. +3. Explain that applying records the current Member as claimant and writes canonical Relay/Activity state in the working tree. A synchronized claim prevents another take; separate offline claims still require Git conflict resolution. There is no unclaim or reassignment action, and it does not assign or start a task elsewhere. 4. Wait for fresh explicit confirmation, then apply the exact preview with `mex relay acknowledge --apply --json`. 5. Return `/relays?view=mine&state=open&relay=`. diff --git a/.claude/skills/mex-inbox/.mex-managed.json b/.claude/skills/mex-inbox/.mex-managed.json index 3ed7807c..60a0e41e 100644 --- a/.claude/skills/mex-inbox/.mex-managed.json +++ b/.claude/skills/mex-inbox/.mex-managed.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "owner": "mex-agent", "skill": "mex-inbox", - "packageVersion": "0.8.0", + "packageVersion": "0.8.1", "files": { - "SKILL.md": "ec0e61b45c9f2e2d70074bd613c0d294dd4d88ec73964e6187f0ac8d51de1505", - "agents/openai.yaml": "4dbdf205a76ba17d9fd8984c8d546b1d6628f0e135bbaf0c16c773f8fa6fc765", - "references/cli-workflows.md": "5147aa68c8fea7a96dd5c569c23570238e2a32a46655fd14267e31a7c5d12c30" + "SKILL.md": "39cf595ac48203bf49ea2e273ea1c1c25e7ebbf4616c68596cd8e1c08eaf2738", + "agents/openai.yaml": "12a2827fe257c303d926122e4bb590910abd660e6451550a1db68a48037b4402", + "references/cli-workflows.md": "6c91905cc69d8ea47080630ee1551bf1a61ec33605fff01463d4a2ad71f497c6" } } diff --git a/.claude/skills/mex-inbox/SKILL.md b/.claude/skills/mex-inbox/SKILL.md index 4f6e5409..7ddb707c 100644 --- a/.claude/skills/mex-inbox/SKILL.md +++ b/.claude/skills/mex-inbox/SKILL.md @@ -1,24 +1,24 @@ --- name: mex-inbox -description: Prepare and manage governed MEX Inbox proposals for durable Spec-family knowledge. Use when the user clearly asks to capture, create, save, or draft a MEX product Spec, requirement, constraint, acceptance criterion, or durable product-spec decision for team review, or explicitly invokes /mex-inbox or $mex-inbox. Do not use for email inboxes, generic notes, vague brainstorming, OpenAPI or test specs mentioned only by the word "spec," architecture pages, conventions, patterns, Workstreams, Relays, arbitrary Wiki pages, or generic memory. +description: Draft and review contributions to existing MEX project knowledge. Use when the user asks to capture a discussion or decision in project knowledge, propose an addition or correction for team review, or explicitly invokes /mex-inbox or $mex-inbox. Also supports existing Spec proposals. Do not activate for brainstorming alone, routine GROW upkeep, email inboxes, session logs, or handoffs. --- # MEX Inbox -Prepare governed Spec-memory proposals. Treat Inbox as a proposal workflow, not email and not a general Wiki mutation system. +Turn an explicit request to retain project knowledge into a focused contribution for review. Inbox proposals are review artifacts; accepted knowledge belongs in the existing Wiki Markdown. Ordinary GROW upkeep can continue directly. ## Keep the scope honest -- Create exactly one `spec.create` or `spec.update` change per draft. -- Support only `spec`, `requirement`, `constraint`, and `acceptance_criterion` entities. -- Reject unsupported durable knowledge honestly. Do not silently translate architecture, conventions, patterns, Workstreams, Relays, or arbitrary Wiki pages into Specs. -- Require a clear durable claim. Do not capture unresolved brainstorming as team memory. +- Create one `knowledge.create` or `knowledge.update` change per draft, for an existing kind: `architecture`, `component`, `convention`, `decision`, `pattern`, or `guide`. +- Existing Spec workflows also support `spec.create` and `spec.update` for `spec`, `requirement`, `constraint`, and `acceptance_criterion`. Choose these only for actual Spec-family intent. +- Capture durable conclusions, their rationale, and useful evidence. Do not dump the conversation or present unresolved ideas as agreed facts. +- Do not route session logs, Relays, or routine GROW edits through Inbox merely because they contain context. ## Prepare a draft -1. Distill the durable knowledge, rationale, and useful evidence from the conversation. -2. Decide whether it creates a new Spec-family entity or updates an existing exact entity. -3. For an update, resolve the canonical target and current revisions before drafting. Never guess an ID or revision. +1. Distill what future agents or teammates need to know from the user's request and discussion. +2. Search existing knowledge first. Prefer correcting or extending the relevant record or section when it already covers the subject; create a new entry when the claim has no suitable home. +3. Read the exact target and current revisions before drafting a correction. Preserve unrelated knowledge when replacing its body. Never guess an ID or revision. 4. Preserve useful files, code, commits, entities, and external links as evidence only when actually available. Never invent provenance, authors, dates, events, or history. 5. Build one request against the action-scoped runtime contract. 6. Run the exact preview flow and summarize the human effect before technical details. @@ -41,4 +41,4 @@ Read [references/cli-workflows.md](references/cli-workflows.md) before executing - Lead with what the proposal means to a person. Do not expose envelopes, hashes, revision machinery, or raw diffs unless diagnosis requires them. - After a local draft write, say exactly what changed and that it remains checkout-only. - After a canonical write, say which Git-tracked MEX artifacts were written to the working tree and that commit/push is still required to share them. Never claim that MEX committed or pushed. -- When MEX context materially affects the answer, add one concise acknowledgement: `MEX context used: .` +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. diff --git a/.claude/skills/mex-inbox/agents/openai.yaml b/.claude/skills/mex-inbox/agents/openai.yaml index 9e0ac8cf..de507237 100644 --- a/.claude/skills/mex-inbox/agents/openai.yaml +++ b/.claude/skills/mex-inbox/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "MEX Inbox" - short_description: "Draft governed MEX Spec proposals" - default_prompt: "Use $mex-inbox to draft this durable requirement for MEX team review." + short_description: "Propose additions to project knowledge" + default_prompt: "Use $mex-inbox to capture what we decided as a contribution to this project's knowledge." diff --git a/.claude/skills/mex-inbox/references/cli-workflows.md b/.claude/skills/mex-inbox/references/cli-workflows.md index 76e79582..c0f9215d 100644 --- a/.claude/skills/mex-inbox/references/cli-workflows.md +++ b/.claude/skills/mex-inbox/references/cli-workflows.md @@ -23,7 +23,17 @@ Use one of these command IDs: Treat this bounded action result as the exact runtime source for the request shape, available examples, constraints, preview command, and apply command. Do not dump `mex capabilities --json` or the full Inbox contract during ordinary execution. Write request and preview JSON only to ordinary regular files inside the checkout or an approved temporary directory; do not use symlinks. -## Resolve a Spec update +## Find existing knowledge and resolve a correction + +1. Use `mex wiki query --limit 10 --json` or `mex wiki list --type --limit 25 --json` to find related records before creating another. +2. Read the best candidate with `mex inbox target --json`. This bounded read returns `target`, `version`, `sourcePath`, and the existing body from the current Wiki index, without initializing or repairing it. +3. For `knowledge.update`, copy `target` and use `version.contentHash` as the target revision and `version.semanticRevision` as the semantic revision. Select a section entity when only that section should change. The patch body replaces the target's body; preserve its relevant existing content. A target can fit the read response but exceed the 16 KiB update-body limit; choose a smaller section or a title/summary-only patch when appropriate, never truncate existing knowledge to fit. +4. If no suitable record exists, use `knowledge.create` with the appropriate existing kind. MEX chooses a path in `context/` or `patterns/`; do not invent an Inbox knowledge category or supply an arbitrary destination path. +5. If several targets remain plausible after reading them, ask which one the user intends. If a read reports a stale or unavailable index, address its explicit maintenance requirement before relying on it; never fabricate revisions or silently refresh an index as part of a read. + +For create-time topics, find them with Wiki reads, then resolve each with `mex inbox target --json` for current revisions. Topics can be dependencies but are not Inbox correction targets. Omit topics when none are needed; normal knowledge creates have no relation editor. + +## Resolve a legacy Spec update 1. Use `mex spec list --json` to identify candidates. 2. Use `mex spec show --json` for the exact candidate. @@ -36,7 +46,7 @@ Do not invent target IDs, relation endpoints, topic IDs, or revisions. For a cre ## Save a checkout-local draft 1. Resolve `inbox.draft.save`. -2. Create a unique operation ID and a request containing one `spec.create` or `spec.update` draft. +2. Create a unique operation ID and a request containing one `knowledge.create` or `knowledge.update` draft (or a Spec change for actual Spec-family intent). 3. For a new draft, provide no unrelated expectations. For an existing draft update, read it with `mex inbox draft show --json` and use its exact current local revision. 4. Preview with `mex inbox draft save --json` and capture the complete successful JSON wrapper unchanged. Require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. 5. Summarize the proposed local effect. If the user asked to create/save/draft, apply with `mex inbox draft save --apply --json` without another confirmation. @@ -60,7 +70,7 @@ The apply writes only checkout-local draft state. It does not create a canonical 5. Apply the exact preview with `mex inbox publish --apply --json`. 6. Return `/inbox?view=review&proposal=`. -Publishing removes the exact local draft after creating the pending proposal. It does not approve the proposal or write the requested Spec change. +Publishing removes the exact local draft after creating the pending proposal. It does not approve the proposal or change accepted knowledge. The proposal is Markdown in the working tree; teammates receive it through Git. ## Review canonical proposals @@ -73,4 +83,4 @@ Use `mex inbox proposal list --json` and `mex inbox proposal show 5. Wait for fresh explicit confirmation. 6. Apply the exact preview with the same command plus `--apply --json`. -Approval writes the proposed Spec-family entity change, proposal decision, Wiki ledger, and Activity records to the working tree. Reject and withdraw make a terminal proposal decision without changing the Spec. Mark stale changes a pending proposal to stale only when MEX proves dependency drift. Repair replaces stale intent, clears prior review, and returns the proposal to pending without changing the Spec. These canonical transitions write Activity records; none commits, pushes, pulls, stages, or notifies teammates. +Approval writes the proposed knowledge change, proposal decision, Wiki ledger, and Activity records to the working tree. The proposal remains review history. Reject and withdraw make a terminal proposal decision without changing knowledge. Mark stale changes a pending proposal to stale only when MEX proves dependency drift. Repair replaces stale intent, clears prior review, and returns the proposal to pending without changing knowledge. These canonical transitions write Activity records; none commits, pushes, pulls, stages, or notifies teammates. diff --git a/.claude/skills/mex-relay/.mex-managed.json b/.claude/skills/mex-relay/.mex-managed.json index 5f6e777e..f1eda046 100644 --- a/.claude/skills/mex-relay/.mex-managed.json +++ b/.claude/skills/mex-relay/.mex-managed.json @@ -2,10 +2,10 @@ "schemaVersion": 1, "owner": "mex-agent", "skill": "mex-relay", - "packageVersion": "0.8.0", + "packageVersion": "0.8.1", "files": { - "SKILL.md": "70fa6eb782225f6357b0046ea9449e80095e6c01bc5e960886a2f60fad7e3f25", + "SKILL.md": "186d1d19c4b513f4b98ee27bf0259744d1ef79ffc758676484596cfe498aa502", "agents/openai.yaml": "9c2e9c2e34d60ec0f4d07089b07d9e49a6c8210c2a3e277593c15a2635f03076", - "references/cli-workflows.md": "98be305e6d941acdd5caa7697317c963343bb62ddcc25af8e129d0af1ea0aaa8" + "references/cli-workflows.md": "56b6e74129cdc6ddb5121480b03e01fd2f1f65b7b8c0b7899552a4a217d48cf5" } } diff --git a/.claude/skills/mex-relay/SKILL.md b/.claude/skills/mex-relay/SKILL.md index a1e76f61..15935610 100644 --- a/.claude/skills/mex-relay/SKILL.md +++ b/.claude/skills/mex-relay/SKILL.md @@ -10,12 +10,12 @@ Prepare durable team handoffs that another engineer can continue from. Never rep ## Prepare a Relay draft 1. Infer the useful session state: a concise summary, current position, completed work, in-progress work, blockers, unresolved questions, next actions, and relevant decisions, files, code, commits, or external links. -2. Resolve intended recipients against active MEX Members. Never fabricate member IDs. -3. Default to a standalone Relay. -4. Include a Workstream only when a real, relevant Workstream already exists. Preserve it as supported typed context; never invent one or turn Relay creation into Workstream creation. +2. For “whoever picks this up,” choose open-to-team. It includes future active project Members; no Member lookup is needed to save this local draft. If the user names people, retain named-recipient intent and resolve exact IDs only when needed. An unresolved named draft may stay recipient-free locally; do not silently publish it to everyone. +3. Default to a standalone Relay. Use the local-save shortcut for a new draft; keep the structured preview/apply path for exact updates and canonical actions. +4. Include an existing relevant Workstream only as typed evidence. Never invent one or turn saving a handoff into Workstream creation. 5. Add optional typed context references only when the referenced IDs, paths, commits, or URLs are known. Never invent provenance. -6. Build one request against the action-scoped runtime contract and run the exact preview. -7. When the user already asked to create, save, or draft the handoff, apply that exact checkout-local draft preview without another confirmation. +6. Resolve the action-scoped runtime contract. For a new draft, `mex relay draft save --from --json` performs the exact local preview/apply internally. For an update, build and preview the structured request. +7. An explicit create/save/draft request authorizes that local write without another confirmation. Publication remains separate. 8. Return `/relays?view=drafts&draft=` and state that the draft is checkout-local and nothing has been delivered or shared. Read [references/cli-workflows.md](references/cli-workflows.md) before executing any Relay mutation. Load only the operation being performed. @@ -31,8 +31,9 @@ Read [references/cli-workflows.md](references/cli-workflows.md) before executing ## Preserve lifecycle meaning -- Treat taking as the explicit canonical acknowledge action by an intended recipient. +- Taking records one claimant: a named active recipient, or any active project Member for an open-to-team Relay. Future Members qualify after joining and receiving the artifact through Git. Separate offline claims can still require Git conflict resolution. +- An inactive Member can be reactivated through the existing Team identity workflow, preserving their ID and older handoffs. Preview and confirm this canonical change separately; never change identity merely to bypass Relay eligibility. - Treat closing as “this handoff no longer needs attention.” Do not claim it completes a linked task, issue, pull request, or Workstream. - After publication, say that Git-tracked Relay and Activity records were written to the working tree. Explain that teammates receive them only after commit/push and their own pull or refresh. - Never claim that MEX sent a notification, committed, pushed, pulled, staged, assigned work, or completed another system's object. -- When MEX context materially affects the answer, add one concise acknowledgement: `MEX context used: .` +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. diff --git a/.claude/skills/mex-relay/references/cli-workflows.md b/.claude/skills/mex-relay/references/cli-workflows.md index 5d9b481d..89f81227 100644 --- a/.claude/skills/mex-relay/references/cli-workflows.md +++ b/.claude/skills/mex-relay/references/cli-workflows.md @@ -20,11 +20,11 @@ Use one of these command IDs: Treat this bounded action result as the exact runtime source for the request shape, available examples, constraints, preview command, and apply command. Do not dump `mex capabilities --json` or the full Relay contract during ordinary execution. Write request and preview JSON only to ordinary regular files inside the checkout or an approved temporary directory; do not use symlinks. -## Resolve recipients and optional context +## Choose an audience and optional context -1. Run `mex member list --active --limit 100 --json`, following bounded cursors when present, and match the user's names or team intent to active Members. -2. Use `mex member show --json` when a candidate needs disambiguation or a current artifact revision is required. -3. Stop and ask for recipient clarification when multiple active Members remain plausible, no active Member matches, or bounded results cannot prove uniqueness. Relay stores individual Members, so never fabricate a group for “the backend team.” +1. For an open handoff or “save for whoever picks it up,” use `audience: "team"` and `recipients: []`. Do not enumerate today's Members; active Members who join later must remain eligible. This setting does not share the local draft. +2. For named people, use `audience: "members"`. Resolve names with `mex member list --active --limit 100 --json` and bounded cursors. Local drafts may retain an empty recipient array while names are unresolved; record the intended name in `unresolvedQuestions` and resolve it before publication. +3. Use `mex member show --json` for disambiguation or exact publication revisions. Ask when named recipients remain ambiguous; never reinterpret a named subset such as “the backend team” as every project Member. 4. Default to no Workstream lookup. If the user named a Workstream or the handoff clearly belongs to an existing one, run `mex workstream list --json` and `mex workstream show --json` to resolve it exactly. 5. In Relay v3, preserve a relevant Workstream as typed entity evidence when the selected contract supports it; do not author the legacy top-level Workstream field. @@ -33,12 +33,23 @@ Never fabricate recipient IDs, entity IDs, revisions, commits, code fingerprints ## Save a checkout-local draft 1. Resolve `relay.draft.save`. -2. Create a unique operation ID and a standalone draft with active recipient references and a concise summary. -3. Include only useful non-empty context sections. Prefer accurate omissions over invented completeness. -4. For a new draft, provide no unrelated expectations. For an existing draft update, read it with `mex relay draft show --json` and use its exact local revision. -5. Preview with `mex relay draft save --json` and capture the complete successful JSON wrapper unchanged. Require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. -6. Summarize recipients and continuation state. If the user asked to create/save/draft, apply with `mex relay draft save --apply --json` without another confirmation. -7. Read the returned draft ID and respond with `/relays?view=drafts&draft=`. +2. Write one temporary JSON object containing the draft content. Include a concise summary, the intended audience, and only useful non-empty context sections. Prefer accurate omissions over invented completeness. +3. For a new draft, run `mex relay draft save --from --operation-id --json`. This validates the content, obtains a signed local preview, and applies that exact preview internally. It never publishes and needs no Member resolution. Do not combine `--from` with a request file or `--apply`. +4. Require `ok: true`, `mode: "apply"`, and `data.applied: true`; read the draft ID from `data.localChanges` and return `/relays?view=drafts&draft=`. The shortcut retains the original preview under `.mex/local/relay-previews/` before applying and removes it after success. An interrupted save can resume with the same operation ID and unchanged content, or the exact `problem.recovery` command. Do not replace the receipt or create another operation to work around an interrupted/conflicting result; if recovery is refused, report it and inspect the existing draft state. + +For example, a new open handoff may contain: + +```json +{ + "audience": "team", + "recipients": [], + "summary": "Continue the parser correction.", + "completed": ["Reproduced the failure with the nested-import fixture."], + "nextActions": ["Check whether the same failure affects exported aliases."] +} +``` + +For an existing draft update, read it with `mex relay draft show --json`, preserve its content, and use its exact local revision in the structured request. Preview with `mex relay draft save --json`; require `ok: true`, `mode: "preview"`, and `data.preview.valid: true`. Apply the complete captured envelope unchanged with `mex relay draft save --apply --json`. An explicit save/update request needs no additional confirmation for this checkout-local write. The apply writes only checkout-local draft state in `.mex/local/team.db`. It does not create a canonical Relay or Activity record, deliver a handoff, commit, push, or notify anyone. Apply before the preview expires; if anything changes or the preview becomes stale, preview again instead of reconstructing it. @@ -51,8 +62,8 @@ The apply writes only checkout-local draft state in `.mex/local/team.db`. It doe ## Publish a Relay -1. Read the exact draft and every active recipient Member required by the selected contract. -2. Resolve `relay.publish`, build exact draft/member expectations, and preview with `mex relay publish --json`. +1. Read the exact draft. For an open-to-team audience, no recipient Member list is needed. For named recipients, require at least one and read each current active Member; an empty named draft must be completed before publication. +2. Resolve `relay.publish`. Team publication expects only the exact local draft revision; named publication also expects every recipient Member revision. Preview with `mex relay publish --json`. The service verifies the current active sender in both cases. 3. Explain that applying replaces the private local draft with canonical Git-tracked Relay and Activity records in the working tree, records the service-observed branch/HEAD/dirty repository state without copying dirty source contents, and does not deliver through a notification service or share before Git commit/push and teammate pull/refresh. 4. Wait for fresh explicit confirmation. 5. Apply the exact preview with `mex relay publish --apply --json`. @@ -60,9 +71,9 @@ The apply writes only checkout-local draft state in `.mex/local/team.db`. It doe ## Take a Relay -1. Resolve the exact published Relay with `mex relay show --json` and verify that the current actor is an intended active recipient. +1. Resolve the exact published Relay with `mex relay show --json`. A team audience allows the current active project Member; a named audience requires the current Member to be listed. An omitted audience on an older Relay means named recipients. 2. Resolve `relay.acknowledge` and preview with `mex relay acknowledge --json`. -3. Explain that applying makes the current recipient the sole claimant, prevents another recipient from taking it, and writes canonical Relay/Activity state in the working tree. There is no unclaim or reassignment action, and it does not assign or start a task elsewhere. +3. Explain that applying records the current Member as claimant and writes canonical Relay/Activity state in the working tree. A synchronized claim prevents another take; separate offline claims still require Git conflict resolution. There is no unclaim or reassignment action, and it does not assign or start a task elsewhere. 4. Wait for fresh explicit confirmation, then apply the exact preview with `mex relay acknowledge --apply --json`. 5. Return `/relays?view=mine&state=open&relay=`. diff --git a/.gitattributes b/.gitattributes index ff3ab990..d12ec612 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,3 +17,6 @@ test/fixtures/wiki/malformed/merge-conflict.md -diff # It lives outside test/fixtures/wiki/ so the P2a oracle's "every fixture is # claimed by an expectation" meta-test keeps meaning exactly what it says. test/fixtures/wiki-migration/** -text + +# Capabilities goldens pin the CLI's exact LF JSON output on every platform. +test/fixtures/capabilities/*.json text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72585f23..32f5555d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: - run: npm ci - run: npm run typecheck - run: npm run test + - name: Test isolated telemetry benchmark transport + if: matrix.node-version == '22.22.0' + run: npm run benchmark:telemetry:test - name: Run evaluator tests if: matrix.node-version == '22.22.0' run: npm run eval:test @@ -87,12 +90,35 @@ jobs: node-version: 22.22.0 cache: npm - run: npm ci - - name: Run focused storage and containment tests + - name: Run focused storage, byte-identity, and containment tests run: >- - npx vitest run + npx vitest run --maxWorkers=1 src/team/local-state/__tests__/team-local-state.test.ts src/team/artifacts/__tests__/filesystem.test.ts + src/team/artifacts/__tests__/filesystem-identity.test.ts src/team/artifacts/__tests__/filesystem-publish-failure.test.ts + src/team/artifacts/__tests__/workflow-repositories.test.ts + src/team/identity/__tests__/member-repository.test.ts + src/team/activity/__tests__/repository.test.ts + src/team/activity/__tests__/legacy.test.ts + src/team/relay/cli/__tests__/local-preview.test.ts + src/team/workflow/__tests__/repository-team-workflow-port.test.ts + test/team-inbox-spec-authoring-real.contract.test.ts + test/capabilities.test.ts + test/telemetry.test.ts + test/telemetry-delivery.test.ts + test/telemetry-context-schema.test.ts + test/telemetry-project-context.test.ts + test/telemetry-project-capture.test.ts + src/logging/__tests__/policy.test.ts + src/wiki/operations/__tests__/durability.test.ts + src/wiki/operations/__tests__/identity-precision.test.ts + src/wiki/operations/__tests__/integrity.test.ts + src/wiki/index/__tests__/maintenance.test.ts + src/graph/__tests__/candidate-process.test.ts + src/graph/__tests__/fingerprint-store-transactions.test.ts + src/graph/__tests__/store-statements.test.ts + src/graph/__tests__/engine-continuity-resources.test.ts - run: npm run build - name: Smoke test the packed install run: npm run test:hub:package @@ -108,7 +134,12 @@ jobs: node-version: 22.22.0 cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - name: Install Playwright Chromium + run: | + # Playwright downloads its own Chromium. Exclude the unused runner + # Chrome apt source so unrelated metadata errors cannot block setup. + sudo rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources + npx playwright install --with-deps chromium - run: npm run test:hub:web - run: npm run test:hub:e2e - name: Upload browser failure artifacts @@ -139,7 +170,12 @@ jobs: node-version: 22.22.0 cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - name: Install Playwright Chromium + run: | + # Playwright downloads its own Chromium. Exclude the unused runner + # Chrome apt source so unrelated metadata errors cannot block setup. + sudo rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources + npx playwright install --with-deps chromium - run: npm run build - name: Measure the first release-performance attempt env: @@ -179,7 +215,12 @@ jobs: node-version: 22.22.0 cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - name: Install Playwright Chromium + run: | + # Playwright downloads its own Chromium. Exclude the unused runner + # Chrome apt source so unrelated metadata errors cannot block setup. + sudo rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources + npx playwright install --with-deps chromium - run: npm run build - name: Measure the fresh-runner confirmation attempt env: diff --git a/.mex/.tool-configs/.cursorrules b/.mex/.tool-configs/.cursorrules index b6c9d1fe..fd4e3f99 100644 --- a/.mex/.tool-configs/.cursorrules +++ b/.mex/.tool-configs/.cursorrules @@ -51,7 +51,12 @@ After meaningful work, run GROW: - Ground: what changed in reality? - Record: update `.mex/ROUTER.md` and relevant `.mex/context/` files - Orient: create or update a `.mex/patterns/` runbook if this can recur -- Write: bump `last_updated` on changed scaffold files and run `mex log` when rationale matters +- Write: bump `last_updated` on changed scaffold files; optional `mex log` notes follow the logging policy below + +## Agent Logging +Read `mex logging --json` at session start and before optional logging. This checkout-local advisory preference is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. + +When earlier work may inform the task, use `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json` with a known subject or exact recorded file path, or both. These are historical notes, not accepted current knowledge. Verify conclusions before reuse or explicit promotion with their source retained. ## Navigation At the start of every session, read `.mex/ROUTER.md` before doing anything else. diff --git a/.mex/.tool-configs/.windsurfrules b/.mex/.tool-configs/.windsurfrules index b6c9d1fe..fd4e3f99 100644 --- a/.mex/.tool-configs/.windsurfrules +++ b/.mex/.tool-configs/.windsurfrules @@ -51,7 +51,12 @@ After meaningful work, run GROW: - Ground: what changed in reality? - Record: update `.mex/ROUTER.md` and relevant `.mex/context/` files - Orient: create or update a `.mex/patterns/` runbook if this can recur -- Write: bump `last_updated` on changed scaffold files and run `mex log` when rationale matters +- Write: bump `last_updated` on changed scaffold files; optional `mex log` notes follow the logging policy below + +## Agent Logging +Read `mex logging --json` at session start and before optional logging. This checkout-local advisory preference is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. + +When earlier work may inform the task, use `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json` with a known subject or exact recorded file path, or both. These are historical notes, not accepted current knowledge. Verify conclusions before reuse or explicit promotion with their source retained. ## Navigation At the start of every session, read `.mex/ROUTER.md` before doing anything else. diff --git a/.mex/.tool-configs/CLAUDE.md b/.mex/.tool-configs/CLAUDE.md index 78b9291c..db62c6e5 100644 --- a/.mex/.tool-configs/CLAUDE.md +++ b/.mex/.tool-configs/CLAUDE.md @@ -51,7 +51,12 @@ After meaningful work, run GROW: - Ground: what changed in reality? - Record: update `.mex/ROUTER.md` and relevant `.mex/context/` files - Orient: create or update a `.mex/patterns/` runbook if this can recur -- Write: bump `last_updated` on changed scaffold files and run `mex log` when rationale matters +- Write: bump `last_updated` on changed scaffold files; optional `mex log` notes follow the logging policy below + +## Agent Logging +Read `mex logging --json` at session start and before optional logging. This checkout-local advisory preference is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. + +When earlier work may inform the task, use `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json` with a known subject or exact recorded file path, or both. These are historical notes, not accepted current knowledge. Verify conclusions before reuse or explicit promotion with their source retained. ## Navigation At the start of every session, read `.mex/ROUTER.md` before doing anything else. @@ -60,8 +65,10 @@ For full project context, patterns, and task guidance — everything is there. ## MEX agent skills - At the start of every session, read `.mex/AGENTS.md` and `.mex/ROUTER.md` before project work; follow `ROUTER.md` to load only the relevant context. -- Use `/mex-inbox` for durable governed Spec proposals and `/mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; explicit invocation remains available. -- When MEX context materially influences an answer or implementation, include one concise acknowledgement: `MEX context used: .` +- Read `mex logging --json` at session start and before optional logging. Its checkout-local advisory mode is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. +- When earlier work may inform the task, retrieve bounded relevant notes with `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json`, using the known subject or exact recorded file path, or both. Treat matches as historical evidence, not accepted current knowledge; verify conclusions before reuse or explicit promotion with their source retained. +- Use `/mex-inbox` for explicit contributions to project knowledge and `/mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; ordinary GROW upkeep remains available without Inbox. +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. - Do not claim an author, date, or historical event unless the retrieved data actually provides it. - After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share. - Skill activation is not approval for canonical actions. diff --git a/.mex/.tool-configs/copilot-instructions.md b/.mex/.tool-configs/copilot-instructions.md index b6c9d1fe..fd4e3f99 100644 --- a/.mex/.tool-configs/copilot-instructions.md +++ b/.mex/.tool-configs/copilot-instructions.md @@ -51,7 +51,12 @@ After meaningful work, run GROW: - Ground: what changed in reality? - Record: update `.mex/ROUTER.md` and relevant `.mex/context/` files - Orient: create or update a `.mex/patterns/` runbook if this can recur -- Write: bump `last_updated` on changed scaffold files and run `mex log` when rationale matters +- Write: bump `last_updated` on changed scaffold files; optional `mex log` notes follow the logging policy below + +## Agent Logging +Read `mex logging --json` at session start and before optional logging. This checkout-local advisory preference is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. + +When earlier work may inform the task, use `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json` with a known subject or exact recorded file path, or both. These are historical notes, not accepted current knowledge. Verify conclusions before reuse or explicit promotion with their source retained. ## Navigation At the start of every session, read `.mex/ROUTER.md` before doing anything else. diff --git a/.mex/AGENTS.md b/.mex/AGENTS.md index ea4d0dc5..6fb73040 100644 --- a/.mex/AGENTS.md +++ b/.mex/AGENTS.md @@ -1,7 +1,7 @@ --- name: agents description: Always-loaded project anchor. Read this first. Contains project identity, non-negotiables, commands, and pointer to ROUTER.md for full context. -last_updated: "2026-09-06" +last_updated: "2026-09-10" --- # mex @@ -36,16 +36,15 @@ The repo is indexed into `.mex/graph.db`. Use it as a bounded discovery tool alo ## Scaffold Growth After meaningful work, run GROW: ground what changed, record state/context updates, orient with a reusable pattern when warranted, and write updated timestamps/rationale. See `ROUTER.md`. -Keep MEX-context acknowledgements concise and natural; name the specific file, -event log, entity, or Code Graph evidence used. - ## Navigation At the start of every session, read `ROUTER.md` before doing anything else. For full project context, patterns, and task guidance — everything is there. ## MEX context policy -- When MEX context materially influences an answer or implementation, include one concise acknowledgement: `MEX context used: .` +- Read `mex logging --json` at session start and before optional logging. Its checkout-local advisory mode is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. +- When earlier work may inform the task, retrieve bounded relevant notes with `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json`, using the known subject or exact recorded file path, or both. Treat matches as historical evidence, not accepted current knowledge; verify conclusions before reuse or explicit promotion with their source retained. +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. - Do not claim an author, date, or historical event unless the retrieved data actually provides it. - After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share. - Skill activation is not approval for canonical actions. diff --git a/.mex/ROUTER.md b/.mex/ROUTER.md index baf35a76..49ce8a59 100644 --- a/.mex/ROUTER.md +++ b/.mex/ROUTER.md @@ -16,7 +16,7 @@ edges: condition: when starting a task — check the pattern index for a matching pattern file - target: patterns/release-readme-visuals.md condition: when refreshing the release README, badges, community links, or architecture illustrations -last_updated: 2026-09-09 +last_updated: 2026-09-10 --- # Session Bootstrap @@ -65,14 +65,135 @@ Then read this file fully before doing anything else in this session. - Versioned graph snapshot provenance and read-only freshness inspection gate grounding in check, doctor, and dashboard flows without implicit graph sync. - Grounding carries its change signal in Markdown: `grounds_to[].bodyHash` is - optional and additive, written by the capture and MOVED-repair passes from the - graph's own hash, and backfilled for existing scaffolds on the next capture. + optional and additive, written during initial capture or an explicitly + accepted per-entry sync review from the graph's own hash. Legacy backfill + preserves an existing cached baseline, and MOVED repairs carry earlier change + evidence forward rather than accepting the new body. The `_mex_grounded_source` row remains as a cache of that canonical value. The drift checker prefers the committed hash, falls back to the cache for a grounding authored before the field, and resolves the `grounds_to` key path so migrated scaffolds under `mex.grounds_to` are checked rather than skipped. +- The first 0.8.1 release phase is implemented on `codex/0.8.1`: successful + agent exit no longer authorizes baseline renewal. Interactive sync offers + bounded default-no review of individual groundings, releases the graph lease + before asking, and revalidates document bytes and graph facts before applying. + All planned release phases are implemented; the current main integration, + validation status, and remaining release preparation are tracked in + `docs/design/0.8.1-release-plan.md`. +- Phase 2 adds a graph-first Context Hub at `/knowledge`, with every usual + Wiki entity (including unlinked sections), recorded relationships, and direct + code groundings expanding on selection. The list and full-record routes remain + readable. Bounded private graph/code projections never maintain indexes; + completed Graph/Wiki jobs invalidate the Context caches. Home and primary + navigation now emphasize Context, Code, Relays, and Activity while legacy + workflow routes remain readable. Local verification is recorded in the + same release plan; this is local implementation, not a published release. +- Phase 2b extends Inbox into an explicit contribution workflow for existing + Wiki knowledge: architecture, component, convention, decision, pattern, and + guide additions or text corrections, including existing section entities. + Local drafts publish as Markdown proposals; approval writes existing + `context/` or `patterns/` knowledge and carries the proposal source/evidence. + Original attribution and grounding survive corrections. Legacy Spec payloads + and signed recovery remain supported; ordinary GROW upkeep remains direct. + Inbox and its review count are restored in primary navigation; Team sits with + Relays/Activity. The agent skill searches existing knowledge and uses the + fresh, bounded `mex inbox target --json` lookup for exact correction + revisions. This is working-tree implementation, not a published release. +- Phase 3 adds explicit open-to-team Relays, including future active Members, + while preserving named v3 and earlier artifact/recovery formats. New team + handoffs use schema v4; local drafts may defer recipient selection. Hub and + CLI show the audience, current eligibility, and working-tree/Git boundary. + `mex relay draft save --from ` saves a local draft through the + existing signed workflow, retaining a bounded private preview before apply + so an interrupted save can resume exactly. Explicit Member reactivation + preserves the original identity and older handoffs. This is local + implementation on `codex/0.8.1`; release gates remain in the release plan. +- Phase 4 adds checkout-local agent logging preferences through `mex logging` + and Hub Settings, with a quiet significant-events default and managed + instructions for relevant bounded Timeline retrieval. Timeline filters now + honor type, message, and recorded files without initializing project/global + identity. Unknown untyped context files abstain during Wiki migration. + Ordinary Wiki creation/synthesis retain explicit provenance or capture the + operation actor/time/session; completed plain creates replay without duplicate + records. Legacy Team/Spec recovery bytes stay unchanged. This is local + implementation on `codex/0.8.1`. The three missing Settings heap limits are now + calibrated from retained pinned Linux run `34286120355`; subsequent enforcing + CI passed, as recorded below. Evidence is in + `docs/design/settings-heap-calibration.json`. The separately accepted Graph + isolation timing calibration below changes only its five owned time limits. +- Phase 5 narrows shared artifact I/O to exact bytes by default, with explicit + checkout-neutral reads for canonical Team records. Wiki/local revisions remain + exact through Inbox preview and recovery; legacy Timeline IDs remain stable + across LF/CRLF. The four corrected Wiki ownership boundaries and shared Team + artifact locks retain full-width device/inode IDs and preserve replacement + files during failure cleanup. The expanded Windows/macOS CI job supplies real + platform verification; current results and remaining gates are in the release + plan. Graph extraction, public exports, and on-disk schemas are unchanged. +- The completed release phases are shared on `codex/0.8.1` in draft PR #176. + Main's subsequent Graph/FTS5 fixes are integrated; the release plan records + the import-only conflict resolution and distinguishes this main sync from + the isolated Windows changes. Runner verification is attached to the PR. - Explicit graph status, refresh, and isolated rebuild/recovery commands preserve the last trustworthy index behind one cross-process maintenance lease. +- Graph performance work is implemented separately on + `codex/0.8.1-graph-performance`: outer-owned fingerprint publication, fixed + statement reuse, smaller continuity/reference staging, and disposable Hub + candidate construction. The parent retains validation and publication; + ordinary CLI construction remains in process. Implementation, verification, + and actual Hub measurements are in + `docs/design/code-graph-performance-implementation.md`. This targets the + release branch and is not a published release. +- Corrected graph performance PR #180 run `34288560611` passed Node 22/24, + browser, and Windows/macOS portability checks. Two independent pinned runners + confirmed five material Graph timing failures. The product decision accepts + disposable-worker startup latency for a responsive Hub and compiler-memory + release after jobs. Only those five time limits are recalibrated from retained + pinned evidence in `docs/design/graph-maintenance-timing-calibration.json`; + all memory and other limits stay unchanged. A clean enforcing run on the new + calibrated head remains required. Local same-code memory/latency evidence is + retained in `docs/design/graph-isolation-diagnostic.json`. +- Graph performance PR #180 passed the final checks (run `34291831733`) and + merged into `codex/0.8.1` as `d64f171`; the preceding calibration notes are + historical. Main remains separate. +- Telemetry v2, developed on `codex/0.8.1-telemetry`, merged through PR #188 + into `codex/0.8.1` as `0ad565d`. Final head `a37277b` passed all required + checks in run `34378404972`, including Windows and release performance. + Local verification and latency evidence for the initial implementation and + approved project-context follow-up remain in `docs/design/telemetry-v2.md`. + The approved random installation UUID is shared + across CLI and Hub. The user additionally approved existing scaffold UUIDs + for shared-project estimates and configured AI-tool names from project setup; + these cannot establish team size or identify the invoking agent. The bounded + reader never creates or repairs project identity. Content, paths, queries, + names, repository remotes, and contact data remain outside the event catalog. + Namespaced CLI outcomes, explicit Hub actions/pages, and terminal jobs use a + bounded per-user queue and cancellable delivery. Pure discovery/read commands + remain quiet. CLI feedback uses the existing Hub hosted form without an + analytics identity. +- The authorized main sync incorporates 18 upstream commits through + `bc2d40a5b3db15fc27d9d2ee19ca642050bccc04` into `codex/0.8.1`. + Three catch-block conflicts in `src/cli.ts` combine upstream detailed Graph + diagnostics with the release branch's `process.exitCode`/return behavior. Upstream + degraded Graph reads and extraction-relevant config identity are retained; + Hub reads keep strict freshness and isolated candidate publication keeps its + parent-owned authority checks. Build, typecheck, 257 focused tests, and 85 + evaluator tests pass. Full regression passed 3,668 tests with one skip and + four timeouts; all four passed a serial rerun at unchanged limits. The + release-plan addendum records this evidence. Merge `465c192` passed Node + 22/24 and Windows/macOS CI; browser/performance setup failed twice on an + unused Google Chrome apt repository checksum mismatch before tests ran. + CI now removes its legacy `.list` and newer `.sources` entries before + installing Playwright's own Chromium. Final head `0118536` passed all + required checks in run `34385511415`, including browser and release + performance. Main was not updated by this sync. +- Release preparation for 0.8.1 is authorized on `codex/0.8.1`: root package + metadata and capability goldens now use 0.8.1, and changelog/release notes + describe the complete release. Install examples and compatibility guidance + cover managed skill refresh, Relay-v4 teammate upgrades, FTS5, and telemetry. + Translations retain an explicit older-narrative notice while their upgrade + commands and compatibility warnings are refreshed. Verification and sharing + state are tracked in `docs/design/0.8.1-release-plan.md`; merging to main, + tagging, and publication remain outside this checkpoint. - Targeted graph get/query/impact consumers use one provenance-bound immutable snapshot and discard output if graph or exact source identity changes. - Graph reads separate engine identity from bounded, reportable shortfalls. A @@ -184,6 +305,18 @@ Then read this file fully before doing anything else in this session. - Public package-root exports for the provisional team contracts. **Known Issues:** +- Graph construction still rebuilds the full eligible corpus after source + changes and has no peak-memory quota. The branch's actual Hub probe peaked at + about 1,963 MiB combined RSS and retained multi-second pauses around initial + checks and validation/publication despite responsive compiler-phase polling. + Fatal parent exit may leave owned temporary artifacts. The historical + `docs/design/code-graph-resource-investigation.md` explains the bottleneck and + retained-memory experiments; neither it nor the implementation rules out all + native, slow, or repository-specific leaks. Process isolation can increase + aggregate peak memory even while reducing memory left in the surviving Hub. + PR #180's final platform, browser, and release-performance checks passed after + the accepted five-leaf timing calibration; each later integration requires + its own verification. - Graph schema v4 is operational in this checkout, with two partially parsed source files and no failed files. Treat partial graph evidence as degraded and narrow or fall back to source discovery when needed. @@ -214,7 +347,7 @@ Load the relevant file based on the current task. Always load `context/architect For every task, follow this loop: -1. **CONTEXT** — Load the relevant context file(s) from the routing table above. Check `patterns/INDEX.md` for a matching pattern. If one exists, follow it. Narrate what you load: "Loading architecture context..." +1. **CONTEXT** — Load the relevant context file(s) from the routing table above. Check `patterns/INDEX.md` for a matching pattern. If one exists, follow it. 2. **BUILD** — Do the work. If a pattern exists, follow its Steps. If you are about to deviate from an established pattern, say so before writing any code — state the deviation and why. 3. **VERIFY** — Load `context/conventions.md` and run the Verify Checklist item by item. State each item and whether the output passes. Do not summarise — enumerate explicitly. 4. **DEBUG** — If verification fails or something breaks, check `patterns/INDEX.md` for a debug pattern. Follow it. Fix the issue and re-run VERIFY. diff --git a/.mex/context/architecture.md b/.mex/context/architecture.md index be912807..9fc02390 100644 --- a/.mex/context/architecture.md +++ b/.mex/context/architecture.md @@ -23,7 +23,7 @@ edges: # Broad overview: keep this empty unless a claim depends on a few specific symbols. # Entry shape: { node: "function:", fingerprint: "mh:64:" } grounds_to: [] -last_updated: 2026-09-06 +last_updated: 2026-09-09 mex: id: mx_01M1M0CJ5C5XQV0HM5VM787WQS type: architecture @@ -99,7 +99,7 @@ revision: 1 can use Claude Code, Codex, or OpenCode, and prompt-only fallback works with any file-reading agent. - A local browser connects only to the loopback Project Hub and exchanges a one-use bootstrap token for a private session. -- **posthog-node** — optional anonymous CLI telemetry; disabled for the development repository and by `DO_NOT_TRACK=1` or `MEX_TELEMETRY=0`. +- **PostHog ingestion** — optional pseudonymous CLI/Hub events share one random installation UUID. A bounded read-only configuration snapshot can add the existing scaffold UUID for shared-project estimates and known configured AI-tool names; it never identifies the invoking agent or creates project identity. Names, remotes, paths, content, queries, and contact details remain excluded. A bounded per-user outbox and cancellable Node HTTP transport replace the SDK. Development checkouts and opt-out controls disable collection/delivery; see `TELEMETRY.md`. ## MEX agent skills - At the start of every session, read `.mex/AGENTS.md` and `.mex/ROUTER.md` before project work; follow `ROUTER.md` to load only the relevant context. -- Use `$mex-inbox` for durable governed Spec proposals and `$mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; explicit invocation remains available. -- When MEX context materially influences an answer or implementation, include one concise acknowledgement: `MEX context used: .` +- Read `mex logging --json` at session start and before optional logging. Its checkout-local advisory mode is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. +- When earlier work may inform the task, retrieve bounded relevant notes with `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json`, using the known subject or exact recorded file path, or both. Treat matches as historical evidence, not accepted current knowledge; verify conclusions before reuse or explicit promotion with their source retained. +- Use `$mex-inbox` for explicit contributions to project knowledge and `$mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; ordinary GROW upkeep remains available without Inbox. +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. - Do not claim an author, date, or historical event unless the retrieved data actually provides it. - After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share. - Skill activation is not approval for canonical actions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 912f7f20..021b8f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,100 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.8.1] - 2026-09-10 + +### Added + +- A Context graph in the Project Hub showing existing Wiki entities, + relationships, and direct code groundings, with type filters, selection + details, pan/zoom, and a list alternative. +- Inbox contributions for one addition or correction to existing architecture, + component, convention, decision, pattern, or guide knowledge. Local drafts + publish as Git-shareable Markdown proposals; explicit approval writes the + existing Wiki knowledge and retains contribution evidence. +- Open-to-team Relays that eligible active Members can take, including teammates + who join later; local drafts may leave recipients undecided. + `mex relay draft save --from ` shortens local saving through the existing signed workflow. + Hub and CLI show audience, current eligibility, and the sharing boundary. +- Member reactivation with the original identity, plus checkout-local agent + logging preferences in Hub Settings and `mex logging`: `significant` (the + quiet default), `checkpoints`, and `manual`. Managed agent instructions now + retrieve relevant bounded Timeline notes without automatically promoting + those notes to accepted project knowledge. +- Additive Graph ignore globs under `.mex/config.json`'s `graph.ignore`, with + repository-relative validation that behaves consistently across platforms. +- `mex telemetry disable` and `mex telemetry enable`, writing the same `~/.mex/config.json` key as `mex config set telemetry on|off`. `mex telemetry --help` and `mex telemetry status` now name the `DO_NOT_TRACK=1` and `MEX_TELEMETRY=0` env opt-outs and say which one is in effect; previously the only switch lived under `config` and the env vars appeared solely in the first-run notice (#110). + +### Changed + +- Agent instructions and skills mention MEX naturally alongside useful findings + instead of requiring a fixed acknowledgement footer or routine context-loading + narration. +- Hub navigation centers Context, Code, Inbox, Relays, Team, and Activity. + Specs and Workstreams leave primary navigation; existing artifacts and direct + routes remain readable. +- Hub Graph refresh/rebuild constructs candidates in a disposable Node process, + keeping compiler work off the Hub event loop. The parent retains validation, + cancellation, and atomic publication. Reused SQLite statements, smaller + temporary collections, and outer-owned fingerprint transactions reduce + avoidable work. CLI construction remains in process; changed-source builds + still rebuild the eligible corpus, and aggregate peak memory is not capped. +- Targeted CLI Graph reads can report useful results with explicit qualifications + for config drift, partial parses, and excluded changed source files. Incompatible + engine identity still refuses reads; Hub reads retain strict freshness. + Graph config identity now uses extraction-relevant fields, so formatting and + dependency-version-only edits no longer invalidate the index. Unparseable + configuration still falls back to exact bytes. +- Telemetry now records namespaced CLI commands and outcomes, fixed Hub + page/action categories, and terminal job results through a bounded local queue + and cancellable delivery. CLI/Hub share a random installation UUID; optional + metadata contains only an existing scaffold UUID and known configured AI-tool + names. This supports repeat/shared-project estimates, not verified team size + or detection of the invoking agent. Names, remotes, content, paths, search + text, and contact details remain excluded; existing opt-outs apply. +- `mex feedback` opens the same voluntary form as the Hub's Help shape MEX card, + without adding an analytics identity to the form URL. See [TELEMETRY.md](TELEMETRY.md) + for the event catalog, pseudonymous identifiers, exclusions, and opt-outs. + ### Fixed + +- Successful agent exit no longer authorizes grounding baseline renewal. + Interactive sync requires explicit acceptance of individual groundings and + revalidates document/code identity; moved-symbol repair preserves prior change + evidence instead of accepting changed behavior. +- Unknown untyped `context/*.md` files are no longer automatically classified + as architecture. Wiki creation/synthesis preserves supplied provenance or + records the operation's provenance; Inbox corrections retain original + attribution and grounding alongside proposal evidence. +- Setup links selected tools to the scaffold even when their instruction files + already exist or setup resumes without showing the selection menu. Existing + instructions are preserved, and repository self-setup reuses saved tool + choices while preserving authored knowledge. +- Windows artifact handling now uses exact bytes by default, with explicit + checkout-neutral handling for canonical Team records. Wiki revisions and + recovery stay exact, legacy Timeline IDs remain stable across LF/CRLF, and + affected Team/Wiki ownership checks preserve full-width device/inode identity + and replacement files during failure cleanup. +- Graph maintenance can publish otherwise valid candidates with documented + per-file skips or incomplete parses, instead of discarding the entire build. + Failed maintenance reports the diagnostics that blocked publication. - `mex graph` now fails with an actionable message naming the running Node version when the built-in `node:sqlite` module lacks FTS5 support, instead of surfacing SQLite's raw `no such module: fts5` on the first schema statement that needs it. FTS5 availability is not guaranteed by every Node build/version inside the documented `engines` range (#110). - The FTS5 preflight now covers every consumer, not only `mex graph`'s writable open: read-only and immutable graph opens (`mex check`, `graph scope`/`query`/`get`, `impact`) and the wiki index, whose `wiki_fts` table has the same dependency. `mex wiki rebuild-index` reports the new `WIKI_INDEX_FTS5_UNAVAILABLE` diagnostic rather than `WIKI_INDEX_REBUILD_REQUIRED`, which would have sent users round a loop rebuilding an index no rebuild can fix (#110). - The wiki index's two direct read paths — contract status inspection and the read session — also preflight FTS5 now, instead of letting SQLite's raw error escape. Reachable by building the index on one Node and reading it on another (#110). - COMPATIBILITY.md documents the FTS5 requirement, a one-line command to check the Node you actually run, and that the v0.6.3 fallback predates the code graph. The preflight's error message pointed at a document that said nothing about FTS5 (#110). - `mex graph rebuild`/`refresh`/`repair` and `mex wiki rebuild-index` now ensure `.mex/.gitignore` exists before creating a store. Only `mex setup` did this, so building a store in a checkout that had never run setup left `graph.db`, `-wal` and `-shm` untracked, ready for the next `git add -A` to commit (#110). -### Added -- `mex telemetry disable` and `mex telemetry enable`, writing the same `~/.mex/config.json` key as `mex config set telemetry on|off`. `mex telemetry --help` and `mex telemetry status` now name the `DO_NOT_TRACK=1` and `MEX_TELEMETRY=0` env opt-outs and say which one is in effect; previously the only switch lived under `config` and the env vars appeared solely in the first-run notice (#110). +### Compatibility + +- New open-to-team Relays use schema v4. Teammates need MEX 0.8.1 before + consuming those artifacts; existing named v1–v3 Relays and legacy Spec + proposals remain supported. New named Relays continue using v3. +- The Graph store remains schema v4. Ordinary reads never migrate or repair + indexes. Upgrading the npm package does not update project agent instructions; + review `mex skills sync --dry-run`, then run `mex skills sync` in existing + projects to refresh managed skills and anchors and start a new agent session. + A completed 0.8.0 setup does not need to run again solely for this upgrade; + follow any explicit maintenance action reported by `mex graph status`. ## [0.8.0] - 2026-09-02 diff --git a/CLAUDE.md b/CLAUDE.md index b71c2c81..9e3cc0c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,10 @@ ## MEX agent skills - At the start of every session, read `.mex/AGENTS.md` and `.mex/ROUTER.md` before project work; follow `ROUTER.md` to load only the relevant context. -- Use `/mex-inbox` for durable governed Spec proposals and `/mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; explicit invocation remains available. -- When MEX context materially influences an answer or implementation, include one concise acknowledgement: `MEX context used: .` +- Read `mex logging --json` at session start and before optional logging. Its checkout-local advisory mode is `significant` (quiet default: material decisions, risks, blockers, or durable discoveries), `checkpoints` (batch useful notes at task/session boundaries), or `manual` (no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. +- When earlier work may inform the task, retrieve bounded relevant notes with `mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json`, using the known subject or exact recorded file path, or both. Treat matches as historical evidence, not accepted current knowledge; verify conclusions before reuse or explicit promotion with their source retained. +- Use `/mex-inbox` for explicit contributions to project knowledge and `/mex-relay` for durable team handoffs. Invoke them automatically when intent clearly matches; ordinary GROW upkeep remains available without Inbox. +- When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration. - Do not claim an author, date, or historical event unless the retrieved data actually provides it. - After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share. - Skill activation is not approval for canonical actions. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 9dc067b4..a46b626b 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -52,6 +52,44 @@ shipping new versions. If you only use the `mex` CLI, most of this still applies, but CLI flags themselves are best-effort (see [CLI surface](#cli-surface) below). +## Upgrading to 0.8.1 + +Install `mex-agent@0.8.1`, then run `mex skills sync --dry-run` and +`mex skills sync` in each project whose managed agent skills and instructions +you want to update. Review conflicts with locally edited instructions and start +a new agent session afterward. An already completed 0.8.0 setup does not need +to run setup again just for this package upgrade. Installing the package alone +does not change the repository. + +New **open-to-team Relays use artifact schema v4**. Upgrade teammates to 0.8.1 +before exchanging these handoffs; 0.8.0 cannot read the new format. Existing +schema-v1, v2, and v3 Relays remain supported, and newly published named-recipient +Relays continue to use v3. This Relay artifact version is separate from Graph +and checkout-local database schema versions. + +Tracked Markdown remains canonical. Graph/Wiki indexes and `.mex/local/` stay +checkout-local and ignored by Git. Follow the explicit action reported by +`mex graph status` after upgrading; ordinary reads never rebuild or migrate an +index. A successful agent session no longer authorizes replacing a grounding +baseline: existing baselines change only through explicit, scoped acceptance. + +Telemetry now includes CLI and Hub events under one random installation UUID, +with the existing scaffold UUID and configured AI-tool names when available. +These are pseudonymous usage signals, not verified people or team sizes. Use +`mex telemetry inspect` to inspect the catalog and `mex telemetry disable` to +opt out; `DO_NOT_TRACK=1` and `MEX_TELEMETRY=0` also disable collection and +sending. See [TELEMETRY.md](TELEMETRY.md) for payloads, exclusions, and delivery +limits. Existing opt-out preferences remain effective. + +### Nix source package + +The source `flake.nix` takes its version from `package.json`, but its fixed +`npmDepsHash` predates the current dependency lockfile and needs regeneration +and a successful `nix build` before that package can be considered verified. +The 0.8.1 release checks cover the npm installation path; they do not establish +Nix build support. The helper `prefetch-npm-deps package-lock.json` can compute +the dependency hash in an environment where it is available. + ## The public API The only public surface is what's exported from the package entry point: diff --git a/README.es.md b/README.es.md index 94d25682..0904a407 100644 --- a/README.es.md +++ b/README.es.md @@ -26,6 +26,8 @@ MEX mantiene la arquitectura, las decisiones, los requisitos y los traspasos de +> La descripción principal de esta traducción sigue correspondiendo a 0.8.0. Los comandos de instalación y las indicaciones de actualización se han ajustado para 0.8.1; los cambios de producto se documentan en el [README en inglés](README.md) y en las [notas de la versión 0.8.1](RELEASE_NOTES.md). + --- Una persona del equipo sabe por qué existe una restricción. Otra conoce el historial de depuración. Un agente de programación encontró un caso límite importante en una sesión que nadie más leerá. El siguiente compañero tiene que reconstruir todo ese contexto. @@ -100,7 +102,7 @@ MEX requiere **Node.js 22.5 o posterior** y un repositorio Git. El flujo habitua Ejecuta lo siguiente desde la raíz del repositorio: ```bash -npx mex-agent@0.8.0 setup +npx mex-agent@0.8.1 setup ``` La configuración conserva las instrucciones existentes, construye el Code Graph local e instala las integraciones seleccionadas. Puede iniciar una CLI disponible de Claude Code o Codex que hayas seleccionado para poblar la memoria; si este paso queda incompleto, muestra el prompt y se pausa. Una vez poblada la memoria, captura los vínculos al código, construye el índice de la Wiki, valida el resultado y muestra el punto de control de Git. Los agentes conectados tienen sus propios requisitos de instalación, cuenta y red. @@ -115,7 +117,7 @@ Revisa y ejecuta exactamente los comandos `git add` que muestra la configuració ```bash git commit -m "chore: initialize MEX" -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 hub ``` ![Tres pasos para dejar listo el proyecto: ejecutar la configuración, poblar la memoria y después revisar y guardar el punto de control con un commit antes de abrir Hub.](docs/diagrams/readme/setup.svg) @@ -132,9 +134,9 @@ Comparte el commit de configuración revisado mediante un push siguiendo el fluj Clona el repositorio y la rama del equipo, o actualízalos con un pull de Git. Si la configuración de 0.8 está completa y guardada en un commit, construye los índices derivados en tu propia copia de trabajo y abre Hub: ```bash -npx mex-agent@0.8.0 graph rebuild -npx mex-agent@0.8.0 wiki rebuild-index -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 graph rebuild +npx mex-agent@0.8.1 wiki rebuild-index +npx mex-agent@0.8.1 hub ``` Reutiliza la memoria compartida del proyecto; no la regeneres solo para incorporarte. En Team/Members, comprueba la identidad efectiva y, si hace falta, selecciona tu registro de Member existente para establecerla localmente. Si aún no tienes un registro, créalo explícitamente mediante el flujo con revisión y comparte sus archivos canónicos. Members sirve para atribuir contribuciones, no es un sistema de inicio de sesión ni de permisos. @@ -147,7 +149,7 @@ Para configuraciones antiguas o incompletas, sigue primero las instrucciones de ¿Prefieres una instalación global? ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex setup ``` @@ -162,14 +164,14 @@ La oferta interactiva de instalación global al final de la configuración utili ¿Usas MEX para un agente operativo persistente? ```bash -npx mex-agent@0.8.0 setup --mode agent-memory +npx mex-agent@0.8.1 setup --mode agent-memory ``` Esta plantilla independiente aplica el modelo de enrutamiento y mantenimiento de MEX a laboratorios domésticos, infraestructura y espacios de trabajo de agentes de larga duración. Añade un contrato `HEARTBEAT.md` y convenciones de limpieza; el flujo de Code Graph, Wiki y Hub de equipo descrito en este README corresponde al modo predeterminado `code-repo`. -Los ejemplos usan `mex` para facilitar la lectura. Instálalo globalmente como se indica arriba o sustitúyelo por `npx mex-agent@0.8.0`. +Los ejemplos usan `mex` para facilitar la lectura. Instálalo globalmente como se indica arriba o sustitúyelo por `npx mex-agent@0.8.1`. @@ -357,12 +359,14 @@ Usa `mex capabilities --json` para descubrir las capacidades en un formato legib Para una instalación global, actualiza la CLI y las copias de las skills seleccionadas de Claude Code/Codex: ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex skills sync --dry-run mex skills sync ``` -Inicia una nueva sesión del agente después de sincronizar las skills. Actualizar el paquete y las skills no basta para que un repositorio antiguo esté listo para Hub. La implementación de 0.8 permite volver a ejecutar la configuración sobre una estructura ya poblada y conservar los archivos escritos, pero las notas de la versión describen la configuración como un proceso para instalaciones nuevas, no como una garantía de migración universal. Evalúa el proceso de preparación completo con una ejecución de prueba antes de aplicarlo: +Si ya completaste la configuración con 0.8.0, estos comandos actualizan las skills y las instrucciones administradas del agente; no hace falta volver a ejecutar setup solo por actualizar el paquete. Revisa los conflictos que se indiquen con instrucciones editadas localmente y después inicia una nueva sesión del agente. + +Actualizar el paquete y las skills no basta para que un repositorio antiguo o incompleto esté listo para Hub. Para esos repositorios, evalúa setup con una ejecución de prueba antes de aplicarlo; setup conserva los archivos existentes y sigue requiriendo la revisión de sus cambios: ```bash mex setup --dry-run @@ -376,7 +380,7 @@ Revisa cada cambio generado antes de hacer commit. En particular, confirma que ` Las estructuras de Markdown existentes siguen siendo válidas y las lecturas del Graph nunca migran un almacén de forma implícita. Los almacenes compatibles con schema-v2 y los almacenes completos con schema-v3 pueden actualizarse mediante una reparación explícita; los almacenes con schema-v1, parciales, ambiguos, malformados o corruptos requieren una reconstrucción. Sigue la acción exacta que indique `mex graph status`. No añadas una regla general para ignorar `.mex/`: ocultaría la memoria canónica que tu equipo debe compartir. > [!WARNING] -> Coordina la actualización a 0.8 en todo el equipo antes de intercambiar Relays con schema-v3: los binarios anteriores a 0.8 no pueden interpretarlos. Quienes usen Node 20 deben permanecer en MEX 0.6.3 hasta que puedan pasar a Node 22.5 o posterior. +> Actualiza a todo el equipo a **0.8.1 antes de compartir nuevos Relays abiertos al equipo**, que usan schema-v4. Los Relays existentes con schema-v1 a schema-v3 siguen siendo compatibles, y los traspasos con destinatarios concretos siguen usando schema-v3. MEX requiere Node 22.5 o posterior con [SQLite FTS5 disponible](COMPATIBILITY.md#sqlite-fts5). Quienes usen Node 20 deben permanecer en MEX 0.6.3 hasta que puedan pasar a una compilación compatible de Node. @@ -384,17 +388,17 @@ Las estructuras de Markdown existentes siguen siendo válidas y las lecturas del MEX no sube sus registros canónicos, Graph, índice de la Wiki, borradores, selección de identidad ni sesiones de Hub a un servicio de MEX. No ofrece transporte automático entre miembros del equipo: el contenido se comparte mediante las operaciones habituales de Git que tú realizas. Hub escucha en la interfaz de loopback y la capa de recuperación local de MEX no requiere credenciales de modelos. -MEX tiene **telemetría seudónima de uso de la CLI**, activada por defecto salvo que la desactives. Cada invocación que cumpla las condiciones envía como máximo un evento. Los campos permitidos por MEX son un identificador aleatorio de la máquina, el nombre del comando, la versión de MEX, el sistema operativo, la versión de Node y, cuando existe una identidad disponible, un identificador de la estructura de MEX; el SDK de PostHog también añade metadatos con el nombre y la versión de su biblioteca. MEX no añade argumentos de comandos, rutas de archivos, nombres de repositorios, contenido de archivos ni direcciones IP al contenido enviado, aunque el servicio receptor puede observar los metadatos habituales del transporte. +MEX tiene **telemetría seudónima de uso de la CLI y del Hub**, activada por defecto salvo que la desactives. Registra nombres de comandos, resultados, acciones explícitas del Hub, categorías de páginas y resultados de trabajos. Ambos usan un UUID aleatorio de instalación para medir el uso recurrente. Cuando están disponibles, también se envían el UUID existente del scaffold para estimar el uso compartido de un proyecto y los nombres de las herramientas de IA seleccionadas en su configuración; estos datos no identifican al agente que ejecuta el comando ni demuestran el tamaño de un equipo. Quien tenga acceso a la configuración puede asociar el UUID del scaffold con ese proyecto. No se envían nombres, remotos de repositorios, argumentos, rutas, contenido, búsquedas ni datos de contacto. El servicio receptor puede observar los metadatos habituales del transporte. Una cola local limitada y una espera breve al terminar la CLI permiten posponer el envío sin cambiar el resultado del comando. Consulta o desactiva la telemetría con: ```bash mex telemetry inspect mex telemetry status -mex config set telemetry off +mex telemetry disable ``` -También puedes desactivarla con `MEX_TELEMETRY=0` o `DO_NOT_TRACK=1`. Consulta la [política de telemetría](https://github.com/mex-memory/mex/blob/v0.8.0/TELEMETRY.md) para conocer los controles y el contenido exacto que se envía. Los agentes de programación conectados a MEX pueden tener su propio comportamiento de red y telemetría; eso depende de esas herramientas, no de MEX. +También puedes desactivarla con `MEX_TELEMETRY=0` o `DO_NOT_TRACK=1`. Consulta la [política de telemetría](TELEMETRY.md) para conocer los controles y el contenido exacto que se envía. Los agentes de programación conectados a MEX pueden tener su propio comportamiento de red y telemetría; eso depende de esas herramientas, no de MEX. diff --git a/README.md b/README.md index 15f060e2..72a74a2a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ MEX keeps your team's architecture, decisions, requirements, and handoffs alongs [![GitHub stars](https://img.shields.io/github/stars/mex-memory/mex?style=flat)](https://github.com/mex-memory/mex/stargazers) [![Website](https://img.shields.io/badge/website-mexmemory.com-4f7cff)](https://mexmemory.com) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/FEdNsQ4Qt4) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mex-memory/mex/blob/v0.8.0/LICENSE) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mex-memory/mex/blob/v0.8.1/LICENSE) [![CI](https://github.com/mex-memory/mex/actions/workflows/ci.yml/badge.svg)](https://github.com/mex-memory/mex/actions/workflows/ci.yml) -[![Node.js >=22.5](https://img.shields.io/badge/Node.js-%3E%3D22.5-339933?logo=node.js&logoColor=white)](https://github.com/mex-memory/mex/blob/v0.8.0/package.json) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6)](https://github.com/mex-memory/mex/blob/v0.8.0/package.json) +[![Node.js >=22.5](https://img.shields.io/badge/Node.js-%3E%3D22.5-339933?logo=node.js&logoColor=white)](https://github.com/mex-memory/mex/blob/v0.8.1/package.json) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6)](https://github.com/mex-memory/mex/blob/v0.8.1/package.json) [![Agent memory](https://img.shields.io/badge/agent%20memory-compatible-6f8cff)](#agent-memory-mode) [![MCP: source only](https://img.shields.io/badge/MCP-source%20only-6f8cff)](#mcp-server) @@ -30,10 +30,10 @@ MEX keeps your team's architecture, decisions, requirements, and handoffs alongs One engineer knows why a constraint exists. Another has the debugging history. A coding agent found an important edge case in a session nobody else will read. The next teammate has to piece it together again. -**What one engineer and their agent learn should become context the next teammate can use.** MEX gives that knowledge a durable home in the repository: readable Markdown, code-linked explanations, reviewed Spec proposals, and structured handoffs. People explore and review it in a local Hub; agents retrieve and help maintain it through project instructions and the CLI. +**What one engineer and their agent learn should become context the next teammate can use.** MEX gives that knowledge a durable home in the repository: readable Markdown, code-linked explanations, reviewed knowledge contributions, and structured handoffs. People explore and review it in a local Hub; agents retrieve and help maintain it through project instructions and the CLI. > [!IMPORTANT] -> **[MEX 0.8](https://github.com/mex-memory/mex/releases/tag/v0.8.0) expands agent memory into team memory:** a local Project Hub, structured Wiki and team workflows, governed Specs, Members, Workstreams, Relays, Activity, and official Claude Code/Codex skills—all connected to the existing Code Graph, grounding, and drift system. +> **[MEX 0.8.1](RELEASE_NOTES.md) makes team context easier to explore and maintain:** a visual Context graph, Inbox contributions to project knowledge, open-to-team Relays, configurable agent logging, safer grounding, and a Hub that can keep serving requests during Graph construction. 💬 **Join the MEX community on Discord** — discuss ideas, get help, share feedback, and show what you're building. @@ -44,9 +44,10 @@ One engineer knows why a constraint exists. Another has the debugging history. A | What the team needs to retain | Where it lives in MEX | | --- | --- | | How the system works, and why | Wiki architecture, decisions, conventions, and patterns, with Code Graph grounding | -| What the product must do | Specs, requirements, constraints, and acceptance criteria; Inbox proposals for governed changes | +| A decision or explanation worth sharing | An Inbox proposal that adds to or corrects existing Wiki knowledge | +| Product requirements already captured | Existing Specs, requirements, constraints, and acceptance criteria remain supported | | Where another engineer should continue | Relays with progress, decisions, blockers, evidence, and next actions | -| The context around an area of work | Workstreams and their recorded state | +| Earlier workflow context | Existing Workstream records remain readable | | Who is involved, and what MEX recorded | Members and Activity history | ![An engineer and their agent contribute shared team memory through Git. A teammate and their agent reuse it in a separate checkout, with their own local indexes.](docs/diagrams/readme/git-sharing.svg) @@ -60,7 +61,7 @@ Working solo? The next person using that memory can be you in a new session. An example: Alex changes webhook retry handling, and Sam will continue the work. Both are active MEX Members in a repository their team has already set up. 1. **Start with the team's context.** Alex asks Codex to inspect the existing architecture, relevant decisions, and code evidence before making the change and running tests. -2. **Keep the useful discoveries.** With Alex's direction, Codex updates the relevant Wiki explanation and code references. If the work changes a durable product requirement, it prepares a separate Inbox proposal for explicit approval. +2. **Keep the useful discoveries.** With Alex's direction, Codex updates the relevant Wiki explanation and code references. For a conclusion the team should review, it prepares an Inbox knowledge proposal for explicit approval. 3. **Prepare and publish the handoff.** Alex asks `$mex-relay` to draft a Relay for Sam: what changed, which tests ran, what remains, and where to look next. She reviews the draft and publication preview in Hub, explicitly publishes it, then reviews, commits, and pushes the code and canonical MEX files through Git. 4. **Continue from shared context.** Sam pulls the relevant branch, updates his local indexes as needed, and opens Hub. He reviews and takes the Relay, then asks his coding agent to read its context and continue. His acknowledgement is another canonical change to commit and push. @@ -70,27 +71,27 @@ The Relay carries the explanation and observed repository state—not the uncomm ## Project Hub -The Hub is where people explore and review their team's memory. Open it to understand a part of the codebase, inspect a proposed Spec change, find a handoff addressed to you, or see recorded team history. +The Hub is where people explore and review their team's memory. Open it to explore connected project knowledge and code, review a proposed knowledge change, find an eligible handoff, or see recorded team history. ![Explore Wiki and Code, review Inbox and Specs, and coordinate Relays and Team members in the local Project Hub.](docs/diagrams/readme/hub.svg) -- **Understand the project:** Overview, Search, Knowledge, Specs, and Code bring explanations and implementation evidence together. -- **Review and carry work forward:** Inbox supports governed Spec proposals; Relays preserve what the next person needs; Workstreams retain the surrounding context. +- **Understand the project:** Home, Search, Context, and Code bring explanations and implementation evidence together. The Context graph shows knowledge entities and relationships; selecting one reveals its direct code groundings and details. +- **Review and carry work forward:** Inbox proposes additions and corrections to project knowledge; Relays preserve what the next person needs. Existing Spec proposals and Workstream records remain readable. - **See who and what:** Team/Members supports attribution and local identity selection. Activity shows accepted MEX workflow events and recorded project notes—not every code edit or Git action. - **Keep context usable:** Health and Jobs expose index status and explicit maintenance. -After setup, run `mex hub`. Each engineer's Hub reads their own checkout and listens on `127.0.0.1`; it is not a shared hosted dashboard. Git brings the team's canonical records into that checkout. Hub protects mutations with a server-side session and CSRF token. Playbooks and Catch Up are marked **Coming Soon**, not available in 0.8. +After setup, run `mex hub`. Each engineer's Hub reads their own checkout and listens on `127.0.0.1`; it is not a shared hosted dashboard. Git brings the team's canonical records into that checkout. Hub protects mutations with a server-side session and CSRF token. Playbook and Catch Up product workflows remain future work. ## Quick start -MEX requires **Node.js 22.5 or newer** and a Git repository. The normal npm flow works on macOS, Linux, Windows Command Prompt, PowerShell, and WSL. +MEX requires **Node.js 22.5 or newer**, SQLite FTS5 support in that Node build, and a Git repository. See [runtime compatibility](COMPATIBILITY.md#sqlite-fts5). The normal npm flow works on macOS, Linux, Windows Command Prompt, PowerShell, and WSL. ### Introduce MEX to your repository Run from the repository root: ```bash -npx mex-agent@0.8.0 setup +npx mex-agent@0.8.1 setup ``` Setup preserves existing instructions, builds the local Code Graph, and installs selected integrations. It can launch an available selected Claude Code or Codex CLI to populate memory; if population remains incomplete, setup prints the prompt and pauses. Once populated, setup captures grounding, builds the Wiki index, validates the result, and prints the Git checkpoint. Connected agents have their own installation, account, and network requirements. @@ -105,7 +106,7 @@ Review and run the exact scoped `git add` commands printed by setup. After commi ```bash git commit -m "chore: initialize MEX" -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 hub ``` ![Three steps to a ready project: run setup, populate memory, then review and commit the checkpoint before opening Hub.](docs/diagrams/readme/setup.svg) @@ -120,9 +121,9 @@ Push the reviewed setup commit through your team's normal Git workflow so teamma Clone or pull the team's repository and branch through Git. For a completed, committed 0.8 setup, build the derived indexes in your own checkout and open Hub: ```bash -npx mex-agent@0.8.0 graph rebuild -npx mex-agent@0.8.0 wiki rebuild-index -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 graph rebuild +npx mex-agent@0.8.1 wiki rebuild-index +npx mex-agent@0.8.1 hub ``` Reuse the shared project memory; do not regenerate it just to join. In Team/Members, check the effective identity and, if needed, choose your existing Member record as a local override. If you do not have a record yet, explicitly create one through the reviewed workflow and share its canonical files. Members are attribution, not a sign-in or permission system. @@ -135,7 +136,7 @@ Older or incomplete setups should follow [upgrade and compatibility](#upgrade-an Prefer a global installation? ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex setup ``` @@ -150,14 +151,14 @@ Setup's final interactive global-install offer uses npm's current `latest` versi Using MEX for a persistent operational agent? ```bash -npx mex-agent@0.8.0 setup --mode agent-memory +npx mex-agent@0.8.1 setup --mode agent-memory ``` This separate template applies MEX's routing and maintenance model to homelab, infrastructure, and long-running agent workspaces. It adds a `HEARTBEAT.md` contract and cleanup conventions; the Code Graph, Wiki, and team-Hub flow described in this README is the default `code-repo` mode. -Examples use `mex` for readability. Install it globally as above or replace it with `npx mex-agent@0.8.0`. +Examples use `mex` for readability. Install it globally as above or replace it with `npx mex-agent@0.8.1`. ## How MEX works @@ -169,6 +170,8 @@ The team's memory is shared; the machinery that retrieves it stays local. MEX se Canonical knowledge is structured Markdown with metadata, relations, sources, provenance, and code groundings; accepted Wiki writes append audit records. The Code Graph and Wiki search index are rebuildable local SQLite views, not shared sources of truth. +In 0.8.1, ordinary Wiki creation and synthesis capture the operation's recorded actor, time, and session as creation provenance while preserving any supplied original attribution. Migration leaves unknown, untyped `context/*.md` files untouched; supply an explicit entity type before migrating them instead of relying on an architecture default. + | Commit and push to share | Keep local or ephemeral; never commit | | --- | --- | | `.mex/config.json`, `.mex/.gitignore` | `.mex/graph.db*` | @@ -207,7 +210,9 @@ mex graph get mex impact requireSession ``` -MEX indexes TypeScript/TSX, JavaScript/JSX, Python, and Rust. Module variants such as `.mts`, `.cts`, `.mjs`, and `.cjs` have partial coverage, and Express is the only framework-specific resolver documented for 0.8. Exact `query`, `get`, and `impact` reads—and Hub Code—require a provably fresh Graph; `scope` can instead return bounded live-text evidence for stale or unindexed files, clearly marked `text-only`. +MEX indexes TypeScript/TSX, JavaScript/JSX, Python, and Rust. Module variants such as `.mts`, `.cts`, `.mjs`, and `.cjs` have partial coverage, and Express is the only framework-specific resolver documented for 0.8. CLI Graph reads can return bounded, clearly labelled degraded evidence when configuration drifts, parsing is incomplete, or changed files must be excluded. Incompatible engines still refuse reads, and Hub Code keeps strict freshness checks. `scope` can also return bounded live-text evidence for stale or unindexed files, marked `text-only`. + +Hub Graph construction runs in a disposable process so compiler work can release its memory when the job exits. This improves responsiveness during construction and reduces retained compiler state; it does not make indexing incremental or guarantee lower combined peak memory. ### Grounding and drift @@ -215,13 +220,13 @@ A Wiki claim can point to a deterministic graph node. MEX stores the node ID and ![A Wiki claim is grounded to a code symbol. Code changes can flag the claim for review.](docs/diagrams/readme/grounding.svg) -Drift is a review signal. It does **not** prove that prose is false, that a code change is wrong, or that a model actually reasoned from retrieved context. +Drift is a review signal. It does **not** prove that prose is false, that a code change is wrong, or that a model actually reasoned from retrieved context. A successful agent session no longer accepts a new baseline automatically: replacing an existing baseline requires explicit review of the selected grounding. ## Agents help maintain the team's memory -Agents are both readers and contributors: they can retrieve the team's existing context, help capture discoveries from real work, and prepare Spec proposals or handoffs for a person to review. They do not independently decide what should be published or shared. +Agents are both readers and contributors: they can retrieve the team's existing context, help capture discoveries from real work, and prepare knowledge proposals or handoffs for a person to review. They do not independently decide what should be published or shared. Setup installs small host-agent instructions that point to `.mex/AGENTS.md` for policy and `.mex/ROUTER.md` for task-relevant context. Agents can query Wiki and Graph evidence, provided the host follows those instructions. @@ -242,15 +247,17 @@ mex skills sync --tool codex Use `--tool claude` for Claude Code. Review the resulting instruction and skill files, commit and push them if the team should share the integration, and start a new agent session. -Instructions may select Inbox or Relay from clear natural-language intent, but skill activation never approves a canonical write. When MEX context materially informs work, the agent names the records used; this is transparency, not proof of reasoning. +Instructions may select Inbox or Relay from clear natural-language intent, but skill activation never approves a canonical write. When MEX context materially helps, the agent mentions MEX and the relevant finding naturally, tying it to what it helped understand, decide, or verify, without a standard footer. -The governed Inbox path applies to Spec-family proposals. Ordinary Wiki and context updates do not all pass through Inbox; review those working-tree changes through your normal engineering workflow. +With the 0.8.1 Inbox update, “use MEX Inbox to capture what we decided” produces a contribution to existing project knowledge. The agent checks existing records, then drafts one addition or correction. Architecture, components, conventions, decisions, patterns, and guides are supported alongside existing Spec proposals. A local draft stays in the checkout; publishing writes a Markdown proposal for review; approval updates canonical knowledge and retains the proposal as history. Git shares those files with teammates. + +Ordinary GROW, Wiki, and context upkeep remains available without Inbox. Inbox is the explicit contribution path, not another knowledge category.
MCP server — source only -The repository includes an [MCP workspace](https://github.com/mex-memory/mex/tree/v0.8.0/packages/mex-mcp) for local development. It is not published with MEX 0.8; the released agent interface is the `mex-agent` CLI and its project instructions and skills. +The repository includes an [MCP workspace](https://github.com/mex-memory/mex/tree/v0.8.1/packages/mex-mcp) for local development. It is not published with MEX 0.8; the released agent interface is the `mex-agent` CLI and its project instructions and skills.
@@ -260,7 +267,7 @@ The repository includes an [MCP workspace](https://github.com/mex-memory/mex/tre | --- | --- | | Search and retrieve Wiki or Graph evidence | Whether retrieved evidence is sufficient | | Create a checkout-local Inbox draft | Publishing the proposal for repository review | -| Preview a bounded Spec create/update operation | Approving or rejecting the proposed canonical change | +| Preview a bounded knowledge addition or correction | Approving or rejecting the proposed canonical change | | Create a checkout-local Relay draft | Publishing, taking, and closing a handoff | | Suggest context and grounding updates | Reviewing and committing working-tree changes | @@ -275,29 +282,49 @@ These workflows help a team decide what becomes durable knowledge and preserve e | **Members** | Stable contributor records plus a checkout-local “current member” for attribution | Member records use Git; current selection stays local | | **Workstreams** | Durable context around an area of work and its state | Canonical Markdown through Git | | **Specs** | Structured product requirements, constraints, and acceptance criteria | Canonical Markdown through Git | -| **Inbox** | Governed proposals for one bounded Spec-family create or update | Draft local; published proposal and decisions through Git | +| **Inbox** | Proposals for one project-knowledge addition or correction; existing Spec proposals remain supported | Draft local; published proposal and decisions through Git | | **Relays** | Agent-prepared, human-published context handoffs | Draft local; published/taken/closed record through Git | | **Activity** | Accepted MEX workflow history and custom records | Canonical records through Git | Members provide attribution and provenance. They are **not** accounts, authentication, role-based access control, or repository permissions. -### Inbox: propose before changing durable Specs +### Inbox: contribute to project knowledge -The Inbox skill prepares exactly one bounded `spec.create` or `spec.update` proposal for a Spec, requirement, constraint, or acceptance criterion. A local draft can be previewed before it becomes a repository record, and approval applies the reviewed operation to canonical knowledge. +In 0.8.1, the Inbox skill captures one durable conclusion from a discussion as an addition or correction to architecture, components, conventions, decisions, patterns, or guides. It searches existing knowledge first, saves a local draft, and publishes a Markdown proposal for review. Approval changes the existing knowledge area and retains the proposal's source evidence. Existing `spec.create` and `spec.update` proposals remain supported. -![An Inbox draft stays local until publication. Human review and explicit approval turn the proposal into a canonical Spec.](docs/diagrams/readme/inbox.svg) +![The existing Spec proposal path illustrates local drafting, publication, and explicit approval; knowledge contributions follow the same review boundary.](docs/diagrams/readme/inbox.svg) Every canonical proposal transition still needs ordinary commit/push/pull to reach another checkout. Approval, rejection, and withdrawal are terminal; a stale proposal can be repaired back to pending. An author can use the exceptional self-approval flow, so Inbox is designed for explicit approval—not guaranteed peer review. -Inbox is intentionally Spec-family focused in 0.8. It is not a general Wiki editor or a queue for arbitrary notes. +Ordinary GROW upkeep can still update project knowledge directly. Inbox is the explicit contribution-and-review path; session notes remain in the event log. ### Relay: pass the context baton -A Relay packages what the next person needs: the active sender resolved at publication, one to 32 unique active canonical Member recipients, a summary, optional related context such as a Workstream, and observed repository state. That snapshot includes branch and `HEAD` when available, plus a dirty-tree boolean and timestamp. Publication rejects inactive, duplicate, or unresolved recipients; it stores no diff or dirty file contents. +A Relay packages what the next person needs: a summary, progress, blockers, next actions, useful evidence, and observed repository state. The 0.8.1 update adds **Open to team**, including active Members who join later, alongside named recipients. Save a local draft before selecting anyone, or use `mex relay draft save --from draft.json --json` for the shorter agent path. Publication requires an active sender; named handoffs also require one to 32 unique active recipients. The observed state includes branch, `HEAD`, a dirty-tree flag, and timestamp; it stores no diff or dirty file contents. A Relay is a durable handoff, not chat, a live notification, task assignment, or a Jira replacement. -Within one observed repository state, the first successful eligible recipient becomes the sole claimant. There is no cross-clone network lock, so two unsynchronized recipients can claim separately and later meet a Git conflict. Only the active recorded sender or active recorded claimant can close the Relay; deactivating either principal can block closure. Version 0.8 has no decline, reassign, unclaim, reopen, or administrative-override flow. +Within one observed repository state, the first successful eligible Member becomes the sole claimant. There is no cross-clone network lock, so two unsynchronized Members can claim separately and later meet a Git conflict. Only the active recorded sender or active recorded claimant can close the Relay; deactivating either principal can block closure. In 0.8.1, reactivation restores the original Member identity so older handoffs remain usable. There is no decline, reassign, unclaim, reopen, or administrative-override flow. Teammates exchanging new open-to-team Relays need a CLI that supports schema v4. + +### Project notes: logging and reuse + +`mex log` records decisions, notes, risks, and todos in `.mex/events/decisions.jsonl`. Future agents can retrieve relevant entries with `mex timeline`; people can read them as **Project notes** in Hub Activity. These records are historical context. Durable conclusions can be promoted into maintained knowledge explicitly, with their source retained. + +In 0.8.1, **Settings → Agent logging** controls when agents write optional notes in this checkout: + +- **Significant events** (default): decisions, discoveries, and risks worth remembering. +- **Task checkpoints**: batch useful notes when a task or session ends. +- **Only when asked**: write optional notes on an explicit user request. + +The preference guides agents; explicit user requests and mandatory workflow Activity remain independent. It lives under `.mex/local/` and is not shared through Git. Start a new agent session after syncing the updated instructions. + +```bash +mex logging --json +mex logging checkpoints +mex timeline --query "retry" --file src/client.ts --type decision --limit 20 --json +``` + +Timeline retrieval reads the latest 8 MiB / 10,000 log lines, returns at most 200 entries, and caps output at 64 KiB. JSON reports truncation; a filtered empty result does not prove the full history has no match. Retrieval never changes the log or initializes project identity. ## Command map @@ -312,9 +339,9 @@ Run `mex --help` for the complete interface. | Index and retrieve knowledge | `mex wiki rebuild-index`, `mex wiki query `, `mex wiki show `, `mex wiki related `, `mex wiki backlinks `, `mex wiki for-code ` | | Synthesize or maintain the Wiki | `mex wiki build`, `mex wiki prepare --stage [--cluster ]`, `mex wiki validate`; `mex wiki propose ` and `mex wiki apply ` preview by default and write only with `--apply` | | Review team memory | `mex member --help`, `mex activity --help`, `mex workstream --help`, `mex spec --help` | -| Govern Spec proposals | `mex inbox draft --help`, `mex inbox publish --help`, `mex inbox proposal --help` | +| Propose knowledge additions or corrections | `mex inbox draft --help`, `mex inbox publish --help`, `mex inbox proposal --help` | | Prepare and receive handoffs | `mex relay draft --help`, `mex relay publish --help`, `mex relay acknowledge --help`, `mex relay close --help` | -| Record project notes or manage patterns | `mex log `, `mex timeline`, `mex pattern --help` | +| Record and retrieve project notes | `mex log `, `mex logging --help`, `mex timeline --help`, `mex pattern --help` | | Check and maintain the project | `mex check`, `mex sync`, `mex doctor`, `mex watch` | Use `mex capabilities --json` for machine-readable capability discovery and `mex commands` for the concise CLI map. @@ -324,12 +351,14 @@ Use `mex capabilities --json` for machine-readable capability discovery and `mex For a global installation, upgrade the CLI and refresh the selected Claude Code/Codex skill copies: ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex skills sync --dry-run mex skills sync ``` -Start a new agent session after syncing skills. Package and skill upgrades alone do not make an older repository Hub-ready. The 0.8 implementation can rerun setup against a populated scaffold while preserving authored files, but the release notes describe setup as a fresh-setup path rather than a universal migration guarantee. Evaluate the full-readiness path with a dry run before applying it: +For an already completed 0.8.0 setup, this refreshes the managed skills and agent guidance; setup does not need to run again just for the package upgrade. Review any reported conflicts with locally edited instructions, then start a new agent session. + +Package and skill upgrades alone do not make an older or incomplete repository Hub-ready. For those repositories, evaluate setup with a dry run before applying it; setup preserves authored files and still requires review of its changes: ```bash mex setup --dry-run @@ -343,23 +372,23 @@ Review every generated change before committing. In particular, confirm that `.m Existing Markdown scaffolds remain valid, and Graph reads never migrate a store implicitly. Compatible schema-v2 and complete schema-v3 stores can upgrade through explicit repair; schema-v1, partial, ambiguous, malformed, or corrupt stores require rebuild. Follow the exact action from `mex graph status`. Do not add a broad `.mex/` ignore rule—it would hide the canonical memory your team is meant to share. > [!WARNING] -> Coordinate the 0.8 upgrade across a team before exchanging schema-v3 Relays: pre-0.8 binaries cannot parse them. Node 20 users should remain on MEX 0.6.3 until they can move to Node 22.5 or newer. +> Upgrade teammates to **0.8.1 before sharing new open-to-team Relays**, which use schema v4. Existing v1–v3 Relays remain supported, and named handoffs continue to use v3. Node 20 users should remain on MEX 0.6.3 until they can move to a supported Node build. ## Privacy and trust model MEX does not upload its canonical records, Graph, Wiki index, drafts, identity selection, or Hub sessions to a MEX service. It provides no automatic team transport: sharing happens through normal Git actions you perform. The Hub binds to loopback, and MEX's local retrieval layer requires no model credentials. -MEX has **pseudonymous CLI usage telemetry**, enabled by default unless you opt out. An eligible invocation sends at most one event. MEX's allowlisted fields are a random machine identifier, command name, MEX version, operating system, Node version, and—when an existing identity is available—a scaffold identifier; the PostHog SDK also adds its library name/version metadata. MEX does not add command arguments, file paths, repository names, file contents, or IP addresses to the payload, though the ingestion service can observe ordinary transport metadata. +MEX has **pseudonymous CLI and Hub usage telemetry**, enabled by default unless you opt out. Namespaced commands, outcomes, explicit Hub actions, page categories, and job results help measure feature use and returning installations. Both surfaces reuse one random installation UUID. When available, events also include the existing scaffold UUID to estimate shared-project use and configured AI-tool names from project setup; these do not identify the currently invoking agent or prove team size. Someone with access to project configuration can associate its scaffold UUID with that project. Names, repository remotes, argument values, paths, content, search text, and contact details stay excluded. The ingestion service can observe ordinary transport metadata. Delivery uses a bounded local queue and short CLI cleanup grace; offline telemetry does not change command results. Check or disable telemetry with: ```bash mex telemetry inspect mex telemetry status -mex config set telemetry off +mex telemetry disable ``` -It can also be disabled with `MEX_TELEMETRY=0` or `DO_NOT_TRACK=1`. See the [telemetry policy](https://github.com/mex-memory/mex/blob/v0.8.0/TELEMETRY.md) for the controls and exact payload. Coding agents connected to MEX may have their own network and telemetry behavior; that is governed by those tools, not by MEX. +It can also be disabled with `MEX_TELEMETRY=0` or `DO_NOT_TRACK=1`. See the [telemetry policy](TELEMETRY.md) for the controls, event catalog, privacy boundary, and delivery limits. Coding agents connected to MEX may have their own network and telemetry behavior; that is governed by those tools, not by MEX. ## What MEX is not @@ -381,10 +410,10 @@ MEX keeps team memory in repository files and provides local retrieval and revie ## Explore further -- Read the [MEX 0.8 release notes](https://github.com/mex-memory/mex/releases/tag/v0.8.0). -- Check the [runtime and compatibility guide](https://github.com/mex-memory/mex/blob/v0.8.0/COMPATIBILITY.md) and [security policy](https://github.com/mex-memory/mex/blob/v0.8.0/SECURITY.md). -- Review the [Code Graph support matrix](https://github.com/mex-memory/mex/blob/v0.8.0/docs/code-graph-support.md). -- See the [extractor model and supported relationships](https://github.com/mex-memory/mex/blob/v0.8.0/docs/extractors.md). -- Read the [graph retrieval benchmark results](https://github.com/mex-memory/mex/blob/v0.8.0/evaluate/RESULTS.md), including the blind-graded comparison against an ordinary file-search baseline. +- Read the [MEX 0.8.1 release notes](RELEASE_NOTES.md). +- Check the [runtime and compatibility guide](https://github.com/mex-memory/mex/blob/v0.8.1/COMPATIBILITY.md) and [security policy](https://github.com/mex-memory/mex/blob/v0.8.1/SECURITY.md). +- Review the [Code Graph support matrix](https://github.com/mex-memory/mex/blob/v0.8.1/docs/code-graph-support.md). +- See the [extractor model and supported relationships](https://github.com/mex-memory/mex/blob/v0.8.1/docs/extractors.md). +- Read the [graph retrieval benchmark results](https://github.com/mex-memory/mex/blob/v0.8.1/evaluate/RESULTS.md), including the blind-graded comparison against an ordinary file-search baseline. - Inspect the CLI locally with `mex capabilities --json` and `mex commands`. - Join the [MEX community on Discord](https://discord.gg/FEdNsQ4Qt4) or visit [mexmemory.com](https://mexmemory.com). diff --git a/README.pt-BR.md b/README.pt-BR.md index 1865685c..4891f29e 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -26,6 +26,8 @@ O MEX mantém a arquitetura, as decisões, os requisitos e as passagens de conte +> A descrição principal desta tradução ainda corresponde à versão 0.8.0. Os comandos de instalação e as orientações de atualização foram ajustados para 0.8.1; as mudanças do produto estão documentadas no [README em inglês](README.md) e nas [notas de lançamento da versão 0.8.1](RELEASE_NOTES.md). + --- Uma pessoa sabe por que uma restrição existe. Outra conhece o histórico de depuração. Um agente de programação encontrou um caso extremo importante em uma sessão que ninguém mais vai ler. O próximo colega precisa reconstruir tudo isso. @@ -100,7 +102,7 @@ O MEX requer **Node.js 22.5 ou mais recente** e um repositório Git. O fluxo nor Execute na raiz do repositório: ```bash -npx mex-agent@0.8.0 setup +npx mex-agent@0.8.1 setup ``` A configuração preserva as instruções existentes, constrói o Code Graph local e instala as integrações selecionadas. Ela pode iniciar uma CLI disponível do Claude Code ou do Codex entre as selecionadas para preencher a memória; se esse preenchimento permanecer incompleto, a configuração exibe o prompt e pausa. Depois que a memória está preenchida, a configuração captura o grounding, constrói o índice da Wiki, valida o resultado e exibe o ponto de verificação do Git. Os agentes conectados têm seus próprios requisitos de instalação, conta e rede. @@ -115,7 +117,7 @@ Revise e execute os comandos `git add` com escopo exato exibidos pela configura ```bash git commit -m "chore: initialize MEX" -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 hub ``` ![Três etapas para preparar o projeto: executar a configuração, preencher a memória e revisar e fazer commit do ponto de verificação antes de abrir o Hub.](docs/diagrams/readme/setup.svg) @@ -132,9 +134,9 @@ Faça push do commit de configuração revisado pelo fluxo Git normal da equipe Clone o repositório da equipe ou faça pull da branch pelo Git. Se a configuração da versão 0.8 estiver concluída e incluída em um commit, construa os índices derivados no seu próprio checkout e abra o Hub: ```bash -npx mex-agent@0.8.0 graph rebuild -npx mex-agent@0.8.0 wiki rebuild-index -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 graph rebuild +npx mex-agent@0.8.1 wiki rebuild-index +npx mex-agent@0.8.1 hub ``` Reutilize a memória compartilhada do projeto; não a gere novamente só para entrar na equipe. Em Team/Members, confira a identidade efetiva e, se necessário, selecione seu registro de Member existente para defini-lo localmente. Se você ainda não tiver um registro, crie um explicitamente pelo fluxo com revisão e compartilhe seus arquivos canônicos. Members servem para atribuição, não para login ou controle de permissões. @@ -147,7 +149,7 @@ Configurações antigas ou incompletas devem seguir primeiro a seção de [atual Prefere uma instalação global? ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex setup ``` @@ -162,14 +164,14 @@ A oferta interativa de instalação global ao final da configuração usa a vers Vai usar o MEX com um agente operacional persistente? ```bash -npx mex-agent@0.8.0 setup --mode agent-memory +npx mex-agent@0.8.1 setup --mode agent-memory ``` Esse template separado aplica o modelo de roteamento e manutenção do MEX a ambientes de homelab, infraestrutura e agentes de longa duração. Ele adiciona um contrato `HEARTBEAT.md` e convenções de limpeza; o fluxo de Code Graph, Wiki e Hub de equipe descrito neste README corresponde ao modo padrão `code-repo`. -Os exemplos usam `mex` para facilitar a leitura. Instale-o globalmente como indicado acima ou substitua-o por `npx mex-agent@0.8.0`. +Os exemplos usam `mex` para facilitar a leitura. Instale-o globalmente como indicado acima ou substitua-o por `npx mex-agent@0.8.1`. @@ -357,12 +359,14 @@ Use `mex capabilities --json` para descobrir capacidades em formato legível por Para uma instalação global, atualize a CLI e as cópias das skills selecionadas do Claude Code/Codex: ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex skills sync --dry-run mex skills sync ``` -Inicie uma nova sessão do agente após sincronizar as skills. Atualizar apenas o pacote e as skills não deixa um repositório antigo pronto para o Hub. A implementação da versão 0.8 pode executar novamente a configuração em uma estrutura já preenchida, preservando os arquivos escritos, mas as notas de lançamento descrevem a configuração como um caminho para instalações novas, não como garantia universal de migração. Avalie o caminho completo de preparação com uma simulação antes de aplicá-lo: +Se a configuração com a versão 0.8.0 já foi concluída, esses comandos atualizam as skills e as instruções gerenciadas do agente; não é necessário executar setup novamente só para atualizar o pacote. Revise os conflitos apontados com instruções editadas localmente e depois inicie uma nova sessão do agente. + +Atualizar apenas o pacote e as skills não deixa um repositório antigo ou incompleto pronto para o Hub. Para esses repositórios, avalie setup com uma simulação antes de aplicá-lo; setup preserva os arquivos existentes e suas alterações ainda precisam ser revisadas: ```bash mex setup --dry-run @@ -376,7 +380,7 @@ Revise cada mudança gerada antes de fazer commit. Em particular, confirme que ` As estruturas existentes em Markdown continuam válidas, e as consultas ao Graph nunca migram um armazenamento implicitamente. Armazenamentos compatíveis com schema-v2 e completos com schema-v3 podem ser atualizados por reparo explícito; armazenamentos schema-v1, parciais, ambíguos, malformados ou corrompidos exigem reconstrução. Siga a ação exata indicada por `mex graph status`. Não adicione uma regra ampla para ignorar `.mex/` — ela esconderia a memória canônica que sua equipe deve compartilhar. > [!WARNING] -> Coordene a atualização da equipe para a versão 0.8 antes de trocar Relays schema-v3: binários anteriores à versão 0.8 não conseguem interpretá-los. Usuários do Node 20 devem permanecer no MEX 0.6.3 até poderem migrar para Node 22.5 ou mais recente. +> Atualize toda a equipe para **0.8.1 antes de compartilhar novos Relays abertos à equipe**, que usam schema-v4. Os Relays existentes com schema-v1 a schema-v3 continuam compatíveis, e as passagens de contexto com destinatários específicos continuam usando schema-v3. O MEX exige Node 22.5 ou mais recente com [SQLite FTS5 disponível](COMPATIBILITY.md#sqlite-fts5). Usuários do Node 20 devem permanecer no MEX 0.6.3 até poderem migrar para uma compilação compatível do Node. @@ -384,17 +388,17 @@ As estruturas existentes em Markdown continuam válidas, e as consultas ao Graph O MEX não envia seus registros canônicos, Graph, índice da Wiki, rascunhos, seleção de identidade ou sessões do Hub para um serviço MEX. Ele não oferece transporte automático entre integrantes da equipe: o compartilhamento acontece pelas ações normais do Git que você executa. O Hub escuta no endereço de loopback, e a camada local de consulta do MEX não exige credenciais de modelo. -O MEX tem **telemetria pseudônima de uso da CLI**, ativada por padrão, a menos que você a desative. Uma execução elegível envia no máximo um evento. Os campos permitidos pelo MEX são um identificador aleatório da máquina, nome do comando, versão do MEX, sistema operacional, versão do Node e — quando uma identidade existente está disponível — um identificador da estrutura MEX; o SDK do PostHog também acrescenta metadados com o nome e a versão de sua biblioteca. O MEX não inclui argumentos de comandos, caminhos de arquivos, nomes de repositórios, conteúdo de arquivos ou endereços IP nos dados enviados, embora o serviço de ingestão possa observar os metadados normais de transporte. +O MEX tem **telemetria pseudônima de uso da CLI e do Hub**, ativada por padrão, a menos que você a desative. Registra nomes de comandos, resultados, ações explícitas do Hub, categorias de páginas e resultados de tarefas. Ambos usam um UUID aleatório da instalação para medir o uso recorrente. Quando disponíveis, também são enviados o UUID existente do scaffold para estimar o uso compartilhado de um projeto e os nomes das ferramentas de IA selecionadas na configuração; esses dados não identificam o agente que executa o comando nem comprovam o tamanho da equipe. Quem tem acesso à configuração pode associar o UUID do scaffold àquele projeto. Não são enviados nomes, remotos de repositórios, argumentos, caminhos, conteúdo, pesquisas ou dados de contato. O serviço receptor pode observar metadados normais de transporte. Uma fila local limitada e uma breve espera ao encerrar a CLI permitem adiar o envio sem alterar o resultado do comando. Confira ou desative a telemetria com: ```bash mex telemetry inspect mex telemetry status -mex config set telemetry off +mex telemetry disable ``` -Ela também pode ser desativada com `MEX_TELEMETRY=0` ou `DO_NOT_TRACK=1`. Consulte a [política de telemetria](https://github.com/mex-memory/mex/blob/v0.8.0/TELEMETRY.md) para conhecer os controles e os dados exatos enviados. Os agentes de programação conectados ao MEX podem ter seus próprios comportamentos de rede e telemetria; isso é regido por essas ferramentas, não pelo MEX. +Ela também pode ser desativada com `MEX_TELEMETRY=0` ou `DO_NOT_TRACK=1`. Consulte a [política de telemetria](TELEMETRY.md) para conhecer os controles e os dados exatos enviados. Os agentes de programação conectados ao MEX podem ter seus próprios comportamentos de rede e telemetria; isso é regido por essas ferramentas, não pelo MEX. diff --git a/README.zh-CN.md b/README.zh-CN.md index 9bf454cf..0f286d7d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,6 +26,8 @@ MEX 将团队的架构、决策、需求和交接信息与代码放在一起。 +> 此译文的主要产品介绍仍对应 0.8.0。安装命令和升级指引已调整为 0.8.1;产品变更请参阅[英文 README](README.md)和 [0.8.1 发行说明](RELEASE_NOTES.md)。 + --- 一位工程师知道某条约束为何存在,另一位掌握着调试过程中的来龙去脉。编程智能体发现了一个重要的边界情况,却留在了别人不会再读的会话里。下一位队友只好重新拼凑这些信息。 @@ -100,7 +102,7 @@ MEX 需要 **Node.js 22.5 或更高版本**,以及一个 Git 仓库。标准 n 在仓库根目录运行: ```bash -npx mex-agent@0.8.0 setup +npx mex-agent@0.8.1 setup ``` 设置流程会保留现有指令、构建本地 Code Graph,并安装所选集成。它可以启动已选择且可用的 Claude Code 或 Codex CLI 来填充记忆;如果填充尚未完成,设置流程会打印提示词并暂停。填充完成后,它会采集代码关联信息、构建 Wiki 索引、验证结果,并输出 Git 提交检查点。所连接的智能体需要满足其自身的安装、账户和网络要求。 @@ -115,7 +117,7 @@ git status --short ```bash git commit -m "chore: initialize MEX" -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 hub ``` ![准备好项目的三个步骤:运行设置、填充记忆,然后审阅并提交检查点,再打开 Hub。](docs/diagrams/readme/setup.svg) @@ -132,9 +134,9 @@ npx mex-agent@0.8.0 hub 通过 Git 克隆或拉取团队的仓库和分支。如果 0.8 设置已完成并提交,在自己的检出目录中构建派生索引,然后打开 Hub: ```bash -npx mex-agent@0.8.0 graph rebuild -npx mex-agent@0.8.0 wiki rebuild-index -npx mex-agent@0.8.0 hub +npx mex-agent@0.8.1 graph rebuild +npx mex-agent@0.8.1 wiki rebuild-index +npx mex-agent@0.8.1 hub ``` 复用共享的项目记忆,不要仅为加入项目而重新生成。在 Team/Members 中检查实际生效的身份,并按需选择你已有的 Member 记录作为本地覆盖设置。如果尚无记录,请通过经过审阅的工作流明确创建一条,并共享其权威记录文件。Members 用于标明归属,不是登录或权限系统。 @@ -147,7 +149,7 @@ npx mex-agent@0.8.0 hub 更喜欢全局安装? ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex setup ``` @@ -162,14 +164,14 @@ npm 包名为 `mex-agent`,安装后的命令为 `mex`。运行 `mex hub` 前 想将 MEX 用于长期运行的运维智能体? ```bash -npx mex-agent@0.8.0 setup --mode agent-memory +npx mex-agent@0.8.1 setup --mode agent-memory ``` 这个独立模板将 MEX 的路由与维护模型应用于家庭实验室、基础设施,以及长期运行的智能体工作空间。它增加了 `HEARTBEAT.md` 约定和清理规范;本 README 描述的 Code Graph、Wiki 和团队 Hub 流程属于默认的 `code-repo` 模式。 -为便于阅读,示例使用 `mex`。你可以按上述方式全局安装,或将其替换为 `npx mex-agent@0.8.0`。 +为便于阅读,示例使用 `mex`。你可以按上述方式全局安装,或将其替换为 `npx mex-agent@0.8.1`。 @@ -357,12 +359,14 @@ Relay 是持久交接记录,不是聊天、实时通知、任务分配,也 如果使用全局安装,请升级 CLI 并刷新所选 Claude Code/Codex 技能副本: ```bash -npm install -g mex-agent@0.8.0 +npm install -g mex-agent@0.8.1 mex skills sync --dry-run mex skills sync ``` -同步技能后,开启新的智能体会话。仅升级软件包和技能,并不能让旧仓库立即满足 Hub 的运行条件。0.8 的实现可以在已填充的基础结构上重新运行设置并保留已编写的文件,但发行说明将 setup 描述为全新设置路径,而不是适用于所有情况的迁移保证。请先通过 dry run 评估达到完整就绪状态的路径,再实际应用: +如果已完成 0.8.0 的设置,上述命令会更新受管理的技能和智能体指引;无需仅为升级软件包而重新运行 setup。请检查与本地修改的指引之间报告的冲突,然后开启新的智能体会话。 + +仅升级软件包和技能,并不能让旧仓库或尚未完成设置的仓库立即满足 Hub 的运行条件。对于这些仓库,请先通过 dry run 检查 setup 的变更,再实际应用;setup 会保留现有编写的文件,其变更仍需审阅: ```bash mex setup --dry-run @@ -376,7 +380,7 @@ mex capabilities --json 已有的 Markdown 基础结构仍然有效,Graph 读取从不隐式迁移数据存储。兼容的 schema-v2 和完整的 schema-v3 存储可以通过显式修复升级;schema-v1、不完整、有歧义、格式错误或已损坏的存储则需要重建。请遵循 `mex graph status` 给出的具体操作。不要添加忽略整个 `.mex/` 的规则——这会隐藏团队本应共享的权威记忆文件。 > [!WARNING] -> 在交换 schema-v3 Relays 之前,请协调全团队升级到 0.8:早于 0.8 的程序无法解析该格式。Node 20 用户应继续使用 MEX 0.6.3,直到能够升级至 Node 22.5 或更高版本。 +> **共享新的面向团队开放的 Relay 之前,请先让全团队升级到 0.8.1**,因为这类 Relay 使用 schema-v4。已有的 schema-v1 至 schema-v3 Relay 仍受支持,指定接收人的交接仍使用 schema-v3。MEX 要求 Node 22.5 或更高版本,且[内置 SQLite 必须支持 FTS5](COMPATIBILITY.md#sqlite-fts5)。Node 20 用户应继续使用 MEX 0.6.3,直到能够换用受支持的 Node 构建版本。 @@ -384,17 +388,17 @@ mex capabilities --json MEX 不会将其权威记录、Graph、Wiki 索引、草稿、身份选择或 Hub 会话上传到 MEX 服务。它不提供自动的团队数据传输:共享依靠你执行的常规 Git 操作。Hub 绑定到回环地址,MEX 的本地检索层无需模型凭证。 -MEX 具有**使用假名标识的 CLI 使用遥测**,默认启用,除非你选择退出。符合条件的调用最多发送一个事件。MEX 允许发送的字段包括随机机器标识符、命令名、MEX 版本、操作系统、Node 版本,以及在已有身份可用时的基础结构标识符;PostHog SDK 还会添加其库名称和版本元数据。MEX 不会向载荷添加命令参数、文件路径、仓库名、文件内容或 IP 地址,但接收服务可以观察到常规传输元数据。 +MEX 具有**使用假名标识的 CLI 和 Hub 使用遥测**,默认启用,除非你选择退出。遥测记录命令名称和结果、Hub 的明确操作、页面类别及后台任务结果。两者共用一个随机安装 UUID,以衡量重复使用情况。如果配置可用,事件还会包含已有的 scaffold UUID,用于估算项目的共享使用情况,以及项目配置中选定的 AI 工具名称;这些信息不能识别当前调用命令的智能体,也不能确定团队人数。能访问项目配置的人可以将该 scaffold UUID 与项目关联。不会发送名称、仓库远程地址、参数值、路径、内容、搜索文本或联系方式。接收服务可以观察到常规传输元数据。事件使用有大小限制的本地队列;CLI 退出时只短暂等待,离线发送不会改变命令结果。 通过以下命令检查或关闭遥测: ```bash mex telemetry inspect mex telemetry status -mex config set telemetry off +mex telemetry disable ``` -也可以通过 `MEX_TELEMETRY=0` 或 `DO_NOT_TRACK=1` 禁用。控制选项和精确载荷见[遥测政策](https://github.com/mex-memory/mex/blob/v0.8.0/TELEMETRY.md)。连接到 MEX 的编程智能体可能有自己的网络和遥测行为;这些由相应工具决定,而非 MEX。 +也可以通过 `MEX_TELEMETRY=0` 或 `DO_NOT_TRACK=1` 禁用。控制选项和精确载荷见[遥测政策](TELEMETRY.md)。连接到 MEX 的编程智能体可能有自己的网络和遥测行为;这些由相应工具决定,而非 MEX。 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 667a21cf..d4cd532e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,74 +1,119 @@ -# mex 0.8.0 — Project memory for the whole team +# mex 0.8.1 — Explore context, share knowledge, keep work moving -mex 0.8.0 connects repository-native memory, the local Code Graph and Wiki, -and the Project Hub into one guarded team workflow. It also completes the -fresh-project experience: setup now populates the scaffold with Claude Code or -Codex, finishes both indexes, installs the official skills, validates the -result, and stops at a clear Git checkpoint before Hub starts. +MEX 0.8.1 makes project knowledge easier to explore and contribute to, makes +Relays useful before you know who will pick up the work, and keeps compiler +work from blocking the Hub during Graph construction. ## Highlights -- **Project Hub and team workflows.** The loopback-only Hub now brings together - Members, immutable Activity, Workstreams, governed Inbox proposals for Spec - changes, and standalone Relay handoffs. Canonical changes use exact - preview/review/apply boundaries and successful mutations emit Activity; - member selection and drafts remain checkout-local. -- **Governed Spec authoring and handoffs.** Inbox proposals cover one bounded - Spec create or update at a time, with explicit approval, rejection, - withdrawal, stale detection, and repair. Relays record durable handoffs with - publication-time repository context and an acknowledge/close lifecycle. -- **Graph and Wiki lifecycle.** Setup builds the Graph and completes Wiki - migration and indexing after agent population. Hub Code, Knowledge, and Spec - views read stable index snapshots; ordinary reads never refresh, migrate, or - repair an index. Health surfaces expose only explicit maintenance actions - that are safe for the observed state. -- **Official agent skills.** `mex-inbox` and `mex-relay` ship for Claude Code - and Codex. Setup installs the selected project copies and managed instruction - anchors; `mex skills sync` safely refreshes them after an upgrade without - overwriting user-authored instructions or unrelated skills. -- **Agent-safe discovery.** `mex capabilities --json` reports the bounded - reads, previews, apply operations, and maintenance commands actually - available in the current checkout. - -## Fresh setup - -mex requires Node.js 22.5 or newer. +- **Explore your project's Context graph.** See Wiki entities and recorded + relationships together, filter by type, and select an entity to reveal its + direct code groundings and details. A list view remains available. Inbox, + Relays, Team, and Activity stay in primary navigation; existing Specs and + Workstreams remain readable through their direct routes. +- **Turn a discussion into project knowledge.** Inbox now handles additions + and corrections to existing architecture, components, conventions, decisions, + patterns, and guides. Use `mex-inbox` to prepare a local draft, publish a + Markdown proposal for review, and explicitly approve its contribution to the + Wiki. Evidence and attribution survive; routine knowledge upkeep can still + happen directly. +- **Leave a Relay open to the team.** Eligible active Members can take an open-to-team + handoff, including someone who joins later. Drafts can defer recipient + selection, `mex relay draft save --from ` shortens local saving, + and inactive Members can be reactivated with their original identity. + Audience and sharing states are visible in Hub and CLI. +- **Choose how much agents log.** Hub Settings and `mex logging` offer a quiet + `significant` default, batched `checkpoints`, or `manual` logging. Updated + agent instructions retrieve relevant Timeline notes so useful context is + easier to reuse; a session note does not automatically become accepted Wiki + knowledge. +- **Keep grounding changes deliberate.** A successful agent run no longer + resets drift baselines. Interactive sync asks you to accept individual + groundings and checks that the reviewed document and code still match. + Moving a symbol preserves evidence of earlier changes. + +## Graph and reliability + +Hub refresh/rebuild constructs its Graph candidate in a disposable process, +allowing the Hub to respond during compiler work. The Hub still validates and +publishes the result; initial checks and final publication can still pause it. +Smaller temporary collections and reused database statements remove avoidable +work. This does not add incremental indexing or a peak-memory quota: a changed +source still triggers a build of the eligible corpus, and simultaneous Hub and +worker memory can increase the combined peak. CLI builds remain in process. + +Targeted CLI Graph reads can now explain config drift, partial parses, and +excluded changed source files while returning the remaining useful evidence. +Hub reads retain strict freshness. Formatting or dependency-version-only +changes to configuration no longer invalidate an otherwise unchanged graph. +Maintenance can retain a useful graph with documented per-file gaps, and +failures name the diagnostics that stopped publication. + +Other fixes preserve exact Wiki bytes and full-width filesystem identities on +Windows, stop unknown context files being assumed to be architecture, improve +write provenance, and report missing SQLite FTS5 support clearly. Explicit +Graph/Wiki maintenance also installs ignore protection before creating stores. + +## Telemetry and feedback + +Usage events are **pseudonymous and opt-out**. Namespaced CLI outcomes and +fixed Hub page/action/job events share a random installation UUID. Where +available, events include the existing scaffold UUID and configured tool names +from the allowlist: `claude`, `codex`, `copilot`, `cursor`, `opencode`, and +`windsurf`. Configured tools describe setup, not the invoking agent; multiple +installations on one scaffold are a shared-use signal, not proof of team size. + +Events exclude names, repository remotes, content, paths, queries, and contact +details. A bounded local queue and cancellable delivery keep network waiting +limited; delivery remains best effort. Inspect or disable it with: ```bash -npx mex-agent@0.8.0 setup +mex telemetry inspect +mex telemetry disable ``` -Setup preserves existing authored scaffold files, protects -`.mex/graph.db*`, `.mex/wiki.db*`, and `.mex/local/` from Git, and can launch -the first selected available Claude Code or Codex CLI from the project root. -Review the generated canonical files and use the scoped Git commands printed by -setup. MEX does not stage, commit, push, or pull for you. +`DO_NOT_TRACK=1` or `MEX_TELEMETRY=0` also disables collection and sending. +[TELEMETRY.md](TELEMETRY.md) explains the complete catalog, identifiers, limits, +and opt-outs. `mex feedback` now opens the Hub's existing Help shape MEX form; +any contact details you choose to provide there are separate from telemetry, +and MEX adds no analytics identity to the form URL. -`mex hub` intentionally remains blocked until the current -`.mex/config.json` bytes are committed at Git `HEAD`. The Graph and Wiki -databases and `.mex/local/` state are disposable checkout-local data and should -not be committed. +## Install and upgrade -## Upgrade and compatibility +MEX requires Node.js 22.5 or newer **with SQLite FTS5 support**. The Node version +alone does not guarantee FTS5; [COMPATIBILITY.md](COMPATIBILITY.md#sqlite-fts5) +includes a check for your Node build. + +Start a new project with: ```bash -npm install -g mex-agent@0.8.0 -mex skills sync +npx mex-agent@0.8.1 setup ``` -Installing the npm package alone does not modify a repository. Run -`mex skills sync` only in projects where you want to activate or refresh the -packaged skills, then start a new Claude Code or Codex session so it loads the -new project instructions. +For an existing project: + +```bash +npm install -g mex-agent@0.8.1 +mex skills sync --dry-run +mex skills sync +mex graph status +``` -Compatible schema-v2 and complete schema-v3 Graph stores upgrade losslessly to -schema v4 during explicit maintenance. Schema-v1, partial, or ambiguous stores -require a safe rebuild. Read-only commands never perform the migration. -Existing Markdown scaffolds remain valid. +Run `mex skills sync` inside each project whose installed skills and managed +agent instructions you want to update. Review the dry run and any conflicts +before applying, then start a new agent session. Package installation alone +does not change your project's instructions. A completed 0.8.0 setup does not +need to run again solely for this upgrade; follow any explicit maintenance +action reported by `mex graph status`. -Before a team publishes new schema-v3 Relays, every teammate should update to -mex 0.8.0 because older binaries cannot parse that Relay format. +**Update teammates before using open-to-team Relays.** Those new handoffs use +schema v4 and require MEX 0.8.1. Existing named v1–v3 Relays remain supported; +new named handoffs continue using v3. Legacy Spec proposals also remain usable. +The Graph store stays at schema v4, and ordinary reads never migrate or repair +an index. -The team workflow contracts remain internal rather than new package-root API -exports. Project Hub remains local-only, and Playbook/Catch Up product surfaces, -general Wiki editing UI, and external Relay delivery remain future work. +Project Hub remains local. Local drafts belong to the checkout; published +proposals and handoffs are working-tree files that require Git commit/push/pull +to share. MEX does not perform those Git operations or verify delivery. Tracked +Wiki Markdown remains canonical. Graph/Wiki indexes are rebuildable; drafts +and other `.mex/local/` state stay in the checkout and must not be committed. diff --git a/TELEMETRY.md b/TELEMETRY.md index 003ec014..49ffa1b4 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -1,94 +1,155 @@ # Telemetry -mex collects **anonymous, opt-out** usage data so the maintainer can see how the -tool is actually used — which commands matter, roughly how many people use it, -and whether a project is used by a team. That's the entire purpose. There is no -advertising, no profiling, and no way to tie any data back to a person. - -If you'd rather send nothing, see [How to opt out](#how-to-opt-out) — it's one -command or one environment variable. +MEX collects **pseudonymous, opt-out usage events** to understand feature use, +success rates, and whether installations return. CLI and Hub events share one +random installation UUID. When available, events also carry the project's +existing random scaffold UUID and its configured AI-tool names. An installation +is not a person: two computers have different IDs, and a shared installation +has one ID. Multiple installations using one scaffold can indicate shared +project use, but cannot establish a team or its size. ## What is collected -Every time you run a `mex` command, **one** event is sent containing **exactly** -these fields and nothing else: - -| Field | Example | What it is | -|-------|---------|------------| -| `machine_id` | `3f2a…` (random UUID) | A random ID generated once per machine. Not your username, hostname, or anything derived from you. | -| `scaffold_id` | `9b1c…` (random UUID) | A random ID for the mex scaffold (project). Only present when you run inside a scaffold. Lets us tell "one team on one project" apart from "one person on many machines." | -| `command` | `check` | The command **name only** — e.g. `check`, `sync`, `log`. | -| `mex_version` | `0.5.1` | The installed mex version. | -| `os` | `darwin` | The platform string (`darwin` / `linux` / `win32`). | -| `node_version` | `v22.17.1` | The Node.js version. | - -You can see the literal payload that would be sent, at any time, without sending -anything: +Every event has a name from a fixed catalog, an original UTC timestamp, a random +event UUID for deduplication, and a random installation UUID (`distinct_id` and +`installation_id`). Common properties are `schema_version: 2`, `source` +(`cli` or `hub`), MEX version, OS platform, and Node version. Person-profile +creation and GeoIP enrichment are explicitly disabled. All six events can also +include these optional properties: + +| Property | Source and meaning | +| --- | --- | +| `scaffold_id` | The existing UUIDv4 in the project's `.mex/config.json`; telemetry never creates or repairs it. | +| `configured_ai_tools` | A sorted, unique list of configured tool names: `claude`, `codex`, `copilot`, `cursor`, `opencode`, and/or `windsurf`. This is the project's setup selection, not the agent currently invoking MEX. | + +The metadata reader performs a bounded, read-only snapshot of the existing +project configuration. Ordinary CLI commands reuse one snapshot; `setup` and +`init` reread it at completion so newly saved configuration can appear in the +completion event. The Hub keeps its project's snapshot from the first enabled +event until restart. Missing, malformed, unreadable, or unsafe configuration +omits this metadata. Invalid scaffold IDs and unknown tool names are excluded. +An explicitly empty tool selection can appear as `[]`. MEX does not inspect +running processes or tool accounts to determine which agent is in use. + +| Event | Additional fields | Purpose | +| --- | --- | --- | +| `cli.command_started` | Namespaced command, stage | Count eligible invocations, including those interrupted before completion | +| `cli.command_completed` | Command, stage, outcome, duration in ms | Feature use and success/failure rates | +| `hub.session_started` | None | Count successful Hub starts; not active engagement by itself | +| `hub.page_viewed` | Fixed page category | Understand navigation without sending URLs | +| `hub.action_completed` | Fixed action, stage, outcome, duration in ms; optional apply replay flag | Understand explicit actions and distinguish preview from apply | +| `hub.job_completed` | Fixed job kind, outcome, duration in ms | Understand graph/Wiki job results | + +Commands use names such as `wiki.query` and `relay.draft.save`. Stages are +`preview`, `apply`, or `direct`; the program derives the stage without sending +argument values. Outcomes are `success`, `failure`, or `cancelled` (for Hub +jobs). Durations are capped at 24 hours. CLI completion means the command's +exit status; a successful preview is not a published artifact. For TUI and +interval-watch launchers, completion describes startup of the interface or +watcher, not the end of that later session. + +Hub pages are categories such as `knowledge_detail`, never a knowledge ID or a +URL. Validated Hub actions and terminal job results are recorded on the local +server. Ordinary API reads, polling, SSE, typing, and repeated renders do not +produce usage events. Page events are capped at 60 per minute per Hub process. + +CLI telemetry/configuration controls and explicitly pure discovery/read +surfaces stay silent: `capabilities`, `logging`, `timeline`, Team list/show/ +contract/target reads, Spec reads, and `skills sync`. Hub bootstrap is counted +by the Hub event rather than a second CLI event. Explicit Team mutation +commands are eligible. Unknown commands and invalid options rejected before +an action starts produce no command event. + +Inspect the current catalog, example payloads, queue limits, and existing local +queue metadata without creating an ID, writing a queue, or sending anything: ```bash mex telemetry inspect +mex telemetry status ``` -## What is NEVER collected - -- **No personal data** — no name, email, username, hostname, or git identity. -- **No IP address or location** — geolocation is explicitly disabled. -- **No command arguments, flags, or paths.** -- **No file names or file contents.** -- **No repository name or git remote URL.** - -`machine_id` and `scaffold_id` are random UUIDs. They are **not** derived from -your path, repo, email, or anything identifying — they are just random numbers -that let counts be de-duplicated. - -## Where the data goes - -[PostHog](https://posthog.com) Cloud, **US region** (`https://us.i.posthog.com`). -The ingestion key embedded in mex is write-only — it can send events but cannot -read any data back. - -## When telemetry does NOT run - -Telemetry is automatically disabled — no event sent, no ID file created — when: - -- any opt-out below is active, **or** -- mex is run from a clone of the mex repository itself (so the maintainer's own - development never pollutes the data). - -A telemetry failure (offline, firewall, ad-blocker) never blocks, slows, or -changes the exit code of any command. It is fire-and-forget and fully ignored on -error. - -## How to opt out - -Any **one** of these turns telemetry off completely. When off, no event is ever -sent and the machine-id file is never created. - -| Method | Scope | -|--------|-------| -| `DO_NOT_TRACK=1` | The industry-standard env var. Honored everywhere. | -| `MEX_TELEMETRY=0` | mex-specific env var. | -| `mex config set telemetry off` | Persisted in `~/.mex/config.json` (per-machine). Re-enable with `mex config set telemetry on`. | - -Check the current state and the active opt-out reason any time: +## What is excluded + +MEX does not put names, email addresses, usernames, hostnames, Git identities, +repository names/remotes, file paths, code, knowledge/log content, search text, +raw arguments, receipt tokens, or Member/artifact IDs in events. The only +project identifier is the existing random scaffold UUID described above. +Unknown event properties and values are rejected, including when reading queued +events back from disk. + +Random IDs allow repeat-use measurement; this is pseudonymous data, not a claim +of complete anonymity. A scaffold UUID can be linked to its project by anyone +who also has access to that project's configuration. One person using several +machines, a shared installation, or copied scaffolds retaining an ID can distort +estimates of shared use. Requests go to [PostHog Cloud](https://posthog.com), US +region (`https://us.i.posthog.com/batch/`). MEX does not add an IP address to the +payload. The receiving service can observe the request's IP and ordinary +transport metadata; disabling GeoIP is not a promise that transport metadata +never exists. + +## Delivery and performance + +Events enter a small local queue. Delivery runs opportunistically while a CLI +command works and periodically while the Hub is open. CLI cleanup gives network +delivery a 25 ms grace by default, then cancels outstanding delivery. There is no detached +sender, retry loop, or telemetry daemon. Hub batching uses an unreferenced +15-second timer; each request has a 2-second timeout. A request failure does not +change product output or exit status. + +The queue retains at most 256 events, 256 KiB of event payload, in a database +bounded to 1 MiB. Events older than seven days are discarded on the next queue access and are never sent; without a daemon, an untouched +queue file may physically remain on disk longer. Batches contain at most 32 +events. +A busy or unavailable queue drops telemetry instead of waiting on another +command. Queue and network work still have a small CPU/disk cost; latency is +measured with the actual CLI, including unreachable and hanging servers. + +Delivery is best effort. A short command's final event can remain queued until +another eligible invocation or Hub session; a final invocation might never be +sent. Oldest events are evicted at the bounds. Retries retain the original event +time and UUID. Abrupt termination can leave a start without a completion; +absence of completion does not prove command failure. Do not interpret the +resulting counts as an exhaustive audit log. Local queue expiry does not set +the hosted analytics service's retention period. + +## Opt out + +Any one of these disables collection and sending: ```bash -mex telemetry status +mex telemetry disable +# Equivalent persisted preference: +mex config set telemetry off +# Or an environment override: +DO_NOT_TRACK=1 mex check +MEX_TELEMETRY=0 mex check ``` -## Files mex writes for telemetry - -- `~/.mex/telemetry-id` — your random `machine_id` (mode `0600`). Created only - when telemetry is enabled. Delete it any time; a new one is generated on the - next enabled run. -- `~/.mex/config.json` — your global preferences, including the telemetry - opt-out flag. - -These are separate from a project's `.mex/config.json`, which holds the -project's `scaffold_id`. - -## A note on PostHog metadata - -The PostHog client library attaches two of its own fields to each event — -`$lib` (`posthog-node`) and `$lib_version`. These describe the sending library, -not you, and contain no personal or usage data. +`mex telemetry enable` restores the stored preference and removes the local +opt-out marker; environment overrides still win. The marker prevents unrelated +concurrent preference writes from silently re-enabling telemetry. An unreadable, +malformed, or unsafe existing global preference file also keeps telemetry off; a missing file uses the default. Development +checkouts of MEX and `MEX_DEV` also disable telemetry. +Opt-out is checked before capture and delivery. Explicit disable cancels this +process's delivery and attempts to clear the existing bounded queue. A locked +or damaged queue can prevent cleanup, but the stored opt-out still prevents +sending on subsequent invocations. Already delivered events are not deleted +by a local opt-out. + +Telemetry uses `~/.mex/telemetry-id` (a random UUID, mode `0600`), +`~/.mex/telemetry/outbox.db` (inside a directory created with mode `0700`), and +the global preference in `~/.mex/config.json`. Explicit disable also writes +`~/.mex/telemetry-disabled`, a small opt-out marker removed only by explicit +enable. A short-lived SQLite rollback journal may exist during writes. The +telemetry queue, installation identity, and preferences are local to the user; +telemetry writes nothing to the project's Git artifacts. The existing scaffold +UUID and tool selection are read from project configuration, which may already +be shared through Git. `MEX_HOME` relocates the global `.mex` directory. + +## Voluntary feedback and contact + +`mex feedback` and the Hub's **Help shape MEX** card open the same +[hosted feedback form](https://tally.so/r/KYjv4k). MEX does not collect contact +details in its telemetry or append an installation ID to the form URL. Any +contact details voluntarily provided to that form are handled separately from +usage analytics. The form is managed outside this repository. diff --git a/docs/design/0.8.1-release-plan.md b/docs/design/0.8.1-release-plan.md new file mode 100644 index 00000000..8b482db6 --- /dev/null +++ b/docs/design/0.8.1-release-plan.md @@ -0,0 +1,812 @@ +# MEX 0.8.1 release plan + +Status: All planned feature phases and the main sync are implemented on +`codex/0.8.1`. Final integrated head `0118536` passed all required checks in +[run `34385511415`](https://github.com/mex-memory/mex/actions/runs/34385511415), +including Node 22/24, Windows/macOS, browser, packed installation, and release +performance. This supersedes earlier integration-pending records below. +The user authorized package versioning, changelog, release notes, and associated +upgrade documentation on 2026-09-10. This release-preparation change uses 0.8.1 +and is locally verified; see the final checklist below. Fresh CI is required +on the preparation commit. Nothing is published. +Earlier phase-local and CI-pending entries are retained as historical records; +the latest completed checks supersede their pending status. Each new integrated +head still requires its own checks. +Branch: `codex/0.8.1`, starting from `4942489`. +Draft PR: [#176](https://github.com/mex-memory/mex/pull/176), targeting `main`. + +## Release intent + +Make the existing product easier to understand and dependable in an ordinary +small team: visual repository context, practical Relays, and trustworthy +grounding. Reuse the current engines and contracts. Keep telemetry as a separate +final change, and isolate Windows changes so their effects can be reviewed. + +The feature phases, separate Graph/telemetry PRs, and main sync are integrated +into the release branch. The current authorization prepares release metadata, +documentation, and package verification on this branch. Merging the release +branch into main, tagging, and publication remain outside this checkpoint. + +## Phases + +| Phase | Scope | Done when | Status | +| --- | --- | --- | --- | +| 1. Grounding safety | Remove agent-exit-driven baseline renewal; preserve old evidence; make acceptance explicit and scoped; retain initial capture; keep moved-symbol repair from accepting changed behavior. | No-op agents cannot clear drift; explicit acceptance affects only reviewed entries; setup, legacy grounding, moved symbols, and index rebuild remain covered. | Locally verified | +| 2. Context-focused Hub | Show all knowledge entities in a graph-first Context view, organized by type, with direct code groundings expanding on selection and details in a sidebar. Retain search/filters and a list alternative. Simplify navigation and retire Specs/Workstreams from the primary experience. | The usual 25–35 entities are visible together, including unlinked sections; recorded relationships and code groundings are useful to explore; old artifacts and links remain readable; route and resource budgets hold. | Locally verified | +| 2b. Inbox knowledge contributions | Restore Inbox in navigation; let agents or people propose one addition/correction to existing project knowledge, reuse review/publication, retain evidence, and keep legacy Specs compatible. | A discussion can become a local draft, a Git-shareable Markdown proposal, and an explicitly approved change to existing Wiki knowledge; ordinary GROW upkeep remains available. | Locally verified | +| 3. Practical Relays | Open-to-team audience including future active Members; drafts without recipients; shorter agent/CLI saving; persistent sharing and claim-eligibility information; inactive-member recovery. | An engineer can save context now and an eligible teammate can take it later; local publication is never presented as verified delivery. | Locally verified | +| 4. Focused corrections and knowledge reuse | Unknown-file migration classification, contribution/evidence provenance, confirmed fresh-install bugs, configurable agent logging and relevant log retrieval, docs/translations. | Identified defects are fixed; logging policy is understandable; prior notes can inform work without treating every note as accepted knowledge. | Implemented; calibrated Settings and enforcing CI passed | +| 5. Windows hardening | Two independently reviewable changes: narrow CRLF normalization, then preserve full-width inode identity in affected paths. | Content/revision, preview/recovery, replacement, and lock checks pass, including real Windows coverage. | Implemented; Windows/macOS checks passed before current main sync | +| 6. Telemetry | Separate final change: namespaced CLI commands and outcomes; server-side allowlisted Hub events sharing the approved installation UUID, with optional existing scaffold UUID and configured AI-tool names; exclude names, remotes, Member/artifact IDs, paths, content, search text, and contact details; honor opt-outs. | Payloads, outcomes, opt-outs, and actual CLI latency have dedicated verification. | Merged through PR #188 as `0ad565d`; final required checks passed | + +## Phase 1 boundary + +The safety change is about authorization to replace a baseline. It does not +change code extraction, fingerprint matching, graph schemas, or the public +package API. The expected production changes are in sync, its repair brief, +and the shared grounding runtime. Setup and grounding migration are regression +surfaces because they use that runtime. + +- A successful agent process triggers verification; it does not accept changed + knowledge. +- Existing Markdown baselines and cached old-source evidence survive unless a + specific replacement has been explicitly authorized. +- Initial capture remains available. Legacy backfill uses existing evidence + where it exists instead of silently resetting it to current code. +- Acceptance is scoped to selected groundings and concrete graph facts, using + a small explicit operation rather than a new review workflow. +- Pointer repair preserves earlier change evidence. Smarter automatic + classification of harmless renames is outside this release. + +Initial effort estimate: roughly 2–4 focused engineering days including +regressions; legacy and moved-symbol behavior are the main uncertainties. + +### Phase 1 result + +Interactive sync now presents a default-no review for one grounding at a time, +showing the current document and previous/current code. Serialized grounding +metadata is omitted from the display; approval still binds the complete +document bytes. No graph lease remains held while waiting for the user. A +non-interactive run preserves baselines instead of inferring acceptance. + +Accepted replacements revalidate the document and exact graph facts. Writes +are bounded and reject stale inputs or invalid UTF-8; the canonical document +is published before its cache changes. MOVED repair carries prior evidence +forward, including on failure or an existing destination grounding. + +Local verification: + +- Seven focused files passed: 67 tests covering sync, grounding, setup, + migration, moved symbols, cache preservation, and stale/oversized inputs. +- The two sync/no-op review regressions passed again after the last bounded + reader adjustment. +- An isolated test against starting commit `4942489` reproduced the defect: + code changed from 18% to 21%, capture replaced the baseline, and the document + still said 18%. The identical test passed with the new source. +- `npm run typecheck` passed for the root and both Hub workspaces. +- `npm run build:node` passed, including contract and package declarations. +- `git diff --check` passed. The package-root exports and graph schemas are + unchanged. + +This is Phase 1 verification, not approval to publish 0.8.1. The remaining +phases and integrated release/Windows checks are still pending. + +## Phase 2 graph direction + +- Show every knowledge entity in the normal 25–35 entity repository at once, + including document and section entities without explicit relationships. + Do not collapse these into document groups or stop at the first browse page. +- Use small circular nodes with readable labels beside or below them, thin + curved links, and a spacious organic layout. Adapt the reference to the + existing Hub charcoal background and orange accents. +- Softly group nodes by their recorded entity type within that organic layout + and give types consistent, restrained colors with a textual legend. Type + coloring and grounding health must remain distinguishable. +- Draw existing recorded relationships; expose their type and optional note on + selection. Spatial proximity is grouping, not an authored relationship. +- Selecting a Wiki entity highlights its knowledge connections, opens its + details in the sidebar, and reveals its directly grounded code symbols as + nearby nodes with a distinct code treatment and grounding-edge style. +- Preserve the knowledge layout while code connections expand. Use a short + expansion transition and honor reduced motion. Selecting another Wiki entity + replaces the expanded code nodes; clearing selection collapses them. Selecting + a code node keeps the originating Wiki context open and shows symbol details. +- Use recorded symbol identities, names, and source locations. Preserve missing + or unresolved grounding states; an entity without groundings gets an honest + empty state. Do not expand the full call graph automatically. +- Reuse the existing Wiki and Code engines and freshness checks. Add only the + bounded read projections needed by this view. Keep all ordinary reads free of + maintenance and show when a larger corpus exceeds an established budget. + +Read-only design audit of this repository: 30 Wiki entities, 36 directed +relationships (all `related_to`), and 12 code groundings across seven patterns. +Thirteen document entities are connected; 17 section entities have no explicit +relationships. Topics, sources, and provenance are currently unpopulated. The +first view must be useful with this data, without invented links or attribution. +Grounding freshness was not assessed for this design audit. + +### Phase 2 result + +`/knowledge` now opens the Context graph. All entities in the usual repository +fit in one bounded overview, including disconnected sections. Type colors, +recorded links, local highlighting, pan/zoom/fit, and a detail sidebar are +implemented. Selecting a Wiki node reveals its directly resolved code symbols; +selecting code retains the parent knowledge. Existing list filters, full records, +and legacy workflow routes remain available. + +Two private read-only projections provide a single Wiki overview and compact +per-entity code details. Limits are 100 entities, 500 relationships, 50 direct +code references, and the existing 1 MiB response bound. Partial coverage, +unavailable indexes, missing symbols, and revision mismatches remain explicit. +Reads never refresh indexes or fetch source bodies merely to expand code. +Successful Graph/Wiki jobs invalidate every Context query, including when the +Wiki revision itself stays unchanged. + +Home and primary navigation now emphasize Context, Code, Relays, and Activity. +Specs, Workstreams, and Spec-only Inbox have left the primary navigation; their +existing artifacts and direct routes remain readable. The subsequent Phase 2b +restores Inbox as the explicit contribution path for project knowledge. + +Local verification: + +- Public boundary: `src/index.ts`, package metadata, and dependencies unchanged. +- Read safety and bounds: 72 focused backend/contract tests passed, including + authenticated real Hub reads, immutable snapshots, stale rejection, compact + symbol allowlisting, truncation, and protected file bytes/mtimes. +- Hub tests: 410/410 passed with one worker. An initial concurrent run had three + loading timeouts in existing Inbox/Member tests; both affected suites then + passed independently before the entire serial run passed. +- TypeScript: final `npm run typecheck` passed for root and both Hub workspaces. +- Packaging: full `npm run build` passed; the final frontend changes passed + `build:hub` again. No Graph evaluator/protocol behavior changed in Phase 2. +- Browser: final graph tests passed at 1024 and 1440 pixels, plus production + bootstrap/origin/idle/fixture-exclusion smoke. Earlier focused Home, sidebar, + list, route, keyboard, and accessibility checks passed. Two changed macOS + Home/sidebar screenshots were regenerated and visually reviewed. +- Visual density: the actual 30-record design sample, with code expansion, + produced no overlapping node boxes, browser errors, or page overflow at + 1024 and 1440 pixels. This sample does not assert current grounding freshness. +- Assets: all existing deterministic budgets pass unchanged. Context adds + 25,616 JavaScript bytes and 6,841 CSS bytes; initial assets are 462,634 + JavaScript bytes and 70,574 CSS bytes. Context and the deferred list/detail + remain outside the shell and Home closures. +- Benchmark wiring: 39/39 focused tests passed, including the default graph + read path, populated browser readiness, and separate graph/list asset closures. +- Worktree: `git diff --check` passed; no generated indexes, local state, + dependencies, or built artifacts are included in the local commit checkpoint. + +Pinned Ubuntu runtime/heap/latency gates, packed fresh-install coverage, and +Windows checks remain release gates. The Knowledge read benchmark now targets +`/wiki/graph` and browser readiness requires the populated graph; existing +runtime budgets remain unchanged and are not claimed to pass from local timing. +Phases 3–6 remain unstarted. + +## Phase 2b: explicit knowledge contributions + +Inbox remains a core team feature. It is a contribution workflow into existing +knowledge, with proposals retained as review history. It does not create a new +knowledge category or become mandatory for ordinary GROW/context upkeep. + +- Start from the real request: “capture what we decided in project knowledge.” + Distill durable conclusions and supporting evidence, search existing records, + and prefer a correction or extension when the subject already has a home. +- Reuse checkout-local drafts, portable Markdown proposals, exact signed + preview/apply, review decisions, stale/repair handling, and interrupted-write + recovery. Keep one entity per proposal. +- Add `knowledge.create` and `knowledge.update` for architecture, component, + convention, decision, pattern, and guide. Corrections change title, summary, + or body on an exact existing entity, including a section. New patterns use + `patterns/`; other new knowledge uses `context/`. There is no arbitrary path, + type-conversion, bulk-edit, or new relationship editor. +- Keep existing `spec.create`/`spec.update` proposals readable and reviewable + with their original stored format. Add knowledge support rather than rewrite + old artifacts or broaden the Spec-only direct-write guard. +- Preserve contribution evidence and a link back to the proposal in approved + knowledge. Preserve original attribution and grounding on corrections; do + not infer a producer from a reviewer or accept new grounding baselines. +- Make the sharing stages explicit: a local draft is checkout-only; publishing + writes a pending Markdown proposal into the working tree; approval changes + canonical knowledge. Git distributes canonical files; MEX does not prove + delivery to another checkout. +- Restore Inbox and its pending count in the sidebar, put Team alongside + Relays/Activity, and retain the existing dashboard theme. Update the packaged + Inbox skill and managed instructions for fresh and existing installations. +- Let agents use bounded Wiki search plus `mex inbox target --json` to + obtain existing text and exact revision expectations for a correction. + +This is a bounded extension of the current workflow. Relays, logging settings, +Windows changes, and telemetry remain separate phases. + +### Phase 2b result + +Inbox now accepts one knowledge addition or text correction through the existing +draft, publication, and review flow. The Hub composer supports all six ordinary +knowledge kinds and exact document or section targets. Existing Spec drafts, +proposals, signed envelopes, and recovery remain compatible. Inbox and its review +count are back in primary navigation, with Team beside Relays and Activity. + +Approved knowledge retains the proposal link and supplied evidence. Corrections +preserve original attribution, neighboring sections, and grounding baselines. +The new `mex inbox target` read returns existing text and exact containing-file +revisions from a fresh Wiki observation, with an allowlisted 64 KiB response +limit and no implicit index maintenance. Packaged and installed Inbox skills now +search existing knowledge before drafting a contribution; ordinary GROW upkeep +remains available directly. + +Local verification: + +- All 59 real Inbox contract cases passed across focused runs, including + legacy Spec behavior, each knowledge kind, stale/repair, containment, and + interrupted approval recovery. Initial failures were timeout-only under + concurrent load. Two older aggregate tests were split into seven independent + parameterized cases with identical assertions and unchanged timeouts. +- All 173 focused Wiki writer/model/adapter tests and 17 knowledge safety tests + passed. CLI target tests cover exact hashes, fresh read-only observations, + absent or invalid targets, metadata filtering, and serialized response bounds. +- All 414 Hub tests passed with one worker. Shared contracts, Hub services, + capabilities/schema parity, managed instructions, installer, and skill content + checks passed. No timeouts or resource budgets were widened. +- 27 distinct focused browser checks passed across runs, including local draft + creation, legacy draft editing/publication, review states, responsive layouts, + keyboard navigation, and accessibility. Eleven affected macOS screenshots + were refreshed and visually reviewed; the final Search readiness check passed. +- Final root and workspace typechecks passed. Full `npm run build`, the final + `build:hub`, and the packed fresh-install Hub/official-skills smoke test passed. + Graph evaluator and protocol behavior were unchanged, so evaluator checks + were not applicable to this phase. +- Existing asset budgets pass unchanged: initial JavaScript is 463,641 bytes + and CSS 70,574 bytes; the lazy Inbox route adds 81,140 JavaScript bytes and + 15,454 CSS bytes. Deferred workbenches remain outside Home/shell closures. +- Public package exports and dependencies are unchanged. `git diff --check` + passes; generated indexes, local state, and built output are excluded. + +This phase is included in the local commit checkpoint, including managed skill +and MEX context updates. Nothing has been pushed. Test servers are stopped. +Pinned Ubuntu runtime/heap gates and real Windows coverage remain release +requirements. Phase 3 is next; telemetry stays last and separate. + +## Phase 3 implementation boundary + +- Add explicit `team` or `members` audiences. Preserve omitted audiences as + legacy named-recipient intent. Team eligibility is evaluated when taking the + Relay, including active Members who joined after publication. +- Permit empty recipient arrays in local drafts. Team publication needs only + the exact local draft revision; named publication still needs at least one + active recipient and their exact Member revisions. Both need an active sender. +- Publish open-to-team handoffs as strict schema v4. Keep named publications as + v3 and preserve existing v1–v3 artifacts, signed receipts, and recovery bytes. +- Provide `mex relay draft save --from ` as a local-only shortcut: + validate sparse content, obtain the existing signed preview, and apply it + internally. Retain the structured path for exact draft updates and publication. +- Add explicit Member reactivation using the original ID and existing revision, + alias uniqueness, containment, and Activity checks. Do not add reassignment, + administrative override, or a new identity mechanism. +- Show draft versus canonical working-tree state and eligible takers in Hub/CLI. + Preserve the existing first synchronized claim and close rules; Git still + distributes records and may expose conflicting offline claims. + +### Phase 3 result + +New Hub drafts default to open-to-team. Any active project Member can take a +published team Relay, including someone who joined later. Named handoffs retain +their recipient restrictions. Local drafts may defer recipient selection, and +the Hub selects a newly saved draft so its publication action targets the right +record. Persistent audience and sharing information distinguishes a local draft +from a canonical working-tree artifact without claiming verified delivery. + +`mex relay draft save --from ` accepts sparse content and performs +the existing signed local preview/apply. It retains the original private preview +before applying, allowing an interrupted save to resume without minting a second +handoff. Pending state is bounded; recovery also handles a stranded atomic-write +stage at capacity. Canonical publication still uses explicit preview and apply. + +Inactive Members can be reactivated in Hub or CLI with the same identity, exact +revision, alias checks, and one immutable Activity event. Existing v1–v3 Relays +and signed recovery remain compatible; open-to-team publication uses v4. Teams +need the updated MEX version to read those new v4 artifacts. No general Relay +editor, reassignment, reopening, or administrative override was added. + +Local verification: + +- All 99 distinct real Relay/identity/workflow cases passed across focused + runs, including named v3 and team v4 Git-clone scenarios, future Members, + exact-revision recovery, and interrupted local saves. The two clone cases + initially timed out under concurrent load and then passed in isolation with + unchanged timeouts. The final pending-preview helper passes all 11 tests, + including containment, private files, bounded capacity, exact retries, and + atomic staging debris. +- All 419 Hub tests passed with one worker. Focused shared contracts, Hub + services, Member repositories, codecs, CLI, capability schemas, and official + skill checks passed. No test timeout or resource budget was widened. +- Sixteen distinct scoped browser checks passed, including local save and + publication, Member reactivation, legacy compatibility, keyboard/focus, + accessibility, and 1024/1440-pixel layouts. The changed Relay screenshot was + refreshed and visually reviewed; existing Member screenshots are unchanged. +- Existing asset budgets pass unchanged: initial JavaScript is 463,695 bytes + and CSS 70,574 bytes; the lazy Relays route adds 137,013 JavaScript bytes and + 10,745 CSS bytes. The ready capability manifest remains within 32 KiB by + removing two repetitive Workstream examples while keeping every command and + schema available. +- The official Relay skill is synced to this repository's Claude and Codex + clients; the follow-up dry run reports no changes or conflicts. +- Final full build followed by root and both workspace typechecks passed. + An earlier overlapping typecheck encountered declarations being rebuilt; + sequential verification passed without source changes. Graph evaluator and + protocol behavior are unchanged, so evaluator checks are not applicable. +- The packed fresh-install Hub and official-skills smoke test passed, including + the open-to-team example and sparse local save through the installed CLI. + The saved draft was retrieved by its exact ID/revision; protected Git, source, + knowledge, Member, Activity, Graph, and Wiki state stayed unchanged. +- Public package exports and dependencies are unchanged. `git diff --check` + passes, no files are staged, and generated indexes, local state, and built + output are excluded. Test servers are stopped. + +This phase is committed locally as `02eafd4`; nothing has been pushed. Pinned Ubuntu +runtime/heap gates and real Windows coverage remain release requirements. +Phase 4 results follow below; Windows hardening stays isolated and telemetry stays last. + +## Logging addition + +`mex log` currently appends notes/decisions/risks/todos to +`.mex/events/decisions.jsonl`. These can be read through Timeline and are +projected into Hub Activity as Project notes. The JSONL is not ordinary Wiki +knowledge. Before Phase 4, agent instructions did not reliably retrieve it. + +Keep the change small: + +- Provide an explicit policy for agent logging: `significant` (quiet default), + `checkpoints` (batch at task/session boundaries), and `manual`. Explain that the + policy guides agents; explicit user-requested records must still be saved. +- Expose the logging preference in Hub Settings, backed by the same policy used + by agent instructions and CLI configuration. Show that it applies only to + this checkout; teammates retain their own preference. +- Avoid recording routine tool calls, every edit, or repeated status reports. +- Apply the policy to existing installations through the managed instruction + update path as well as fresh setup; changing templates alone is insufficient. +- Add bounded, relevant timeline retrieval to the agent workflow, using the + task's files or subject rather than loading the entire log on every turn. +- Repair the existing CLI `timeline --type` mapping and the MCP timeline help's + unsupported event kinds while improving retrieval. +- Preserve history. Promote durable conclusions into maintained knowledge + explicitly, with their source retained; do not automatically accept every + historical note as current truth. +- Keep optional agent notes separate from mandatory workflow Activity and + recovery audit records. A quiet policy must never suppress those records. + +### Phase 4 implementation boundary + +- Keep the cadence preference checkout-local in `.mex/local/agent-preferences.json`. + `mex logging --json` reads the effective default/local policy without creating + state. `mex logging significant|checkpoints|manual` changes it explicitly; + the Hub Settings page uses the same exact-revision store. Existing projects + receive dynamic policy/retrieval guidance through managed instruction sync. +- Use bounded `mex timeline --query --file --limit 20 + --json` retrieval for relevant past notes. Preserve the JSONL and public + `readEvents` array API; report when scan or output bounds omit history. Correct + `--type` mapping and MCP supported-kind help. Timeline reads must not initialize + a missing scaffold identity. +- Unknown untyped `context/*.md` files remain untouched during migration until + an explicit type is supplied. Recognized filename rules and existing types + remain supported. +- Capture creation provenance for ordinary Wiki service and synthesis writes + from the operation's recorded actor, time, and session. Preserve supplied + original provenance. Updates keep original creation attribution and retain + their editor audit. Existing Inbox evidence transfer remains covered; legacy + Team/Spec adapter and recovery bytes remain unchanged. +- Fix the confirmed file-level Wiki create retry defect without adding new + authoring workflows. Documentation changes cover these actual behaviors; + unrelated translations and speculative fresh-install issues are not expanded + into a rewrite. Windows and telemetry remain separate phases. + +### Phase 4 result + +Hub Settings and `mex logging` now share three advisory logging modes, with +significant events as the quiet default. Reads do not initialize preferences; +saves bind the exact file bytes and reject stale revisions, including external +line-ending edits. Exact-byte handling is opt-in for this local file; existing +canonical artifact normalization is unchanged pending Phase 5. + +Managed instructions read the preference dynamically and retrieve relevant +historical notes using bounded Timeline filters. The existing-install sync is +applied locally and its follow-up dry run reports no changes or conflicts. +Timeline type filtering is corrected and filtered reads leave project/global +identity and log bytes untouched. Logging and Timeline bypass existing +telemetry/first-run initialization hooks to preserve that read behavior; +telemetry payload and outcome work remains in Phase 6. + +Unknown untyped context files now abstain from migration. Ordinary Wiki creation +and synthesis capture explicit operation provenance without replacing original +attribution, and exact completed create retries return their audit proof without +writing duplicate content. Inbox evidence and legacy Team/Spec recovery retain +their existing contracts. English documentation describes the changes; the +three translations carry an explicit version notice and links to current detail. + +Local verification: + +- All 424 Hub tests passed. The final built CLI, logging-policy, and real Hub + settings batch passed 42 tests; focused containment/atomic replacement checks + also cover stale revisions before and after staging. +- Focused Wiki operation, service, synthesis, migration, and adversarial + regressions passed, including original provenance, unknown-file byte + preservation, exact retry, and legacy recovery behavior. Event, MCP Timeline, + managed instruction, and installer checks passed. +- Twelve scoped browser checks passed: Settings keyboard/focus/accessibility + at 1024 and 1440 pixels, nine navigation/layout compatibility checks, and the + built production Hub smoke. Three affected sidebar screenshots were updated + and visually reviewed. +- Full build and sequential root/Hub workspace typechecks passed. The packed + fresh-install Hub and official-skills smoke passed, including default + noninitializing reads, Hub/CLI preference persistence, stale-save rejection, + and unchanged protected Git/source/knowledge/Member/Activity/index state. +- All 43 release benchmark tests passed. Initial JavaScript is 464,718 bytes + and CSS 70,574 bytes. Settings adds 7,652 JavaScript and 2,771 CSS bytes, within + its new 8,035/2,910-byte limits. Every earlier asset/runtime budget is unchanged. + The three Settings heap limits remain absent until pinned Ubuntu 24.04/Node 22 + calibration; the runtime gate explicitly fails on those missing limits. +- Package-root exports, the public `readEvents` API, and dependencies remain + unchanged. `git diff --check` passes; generated indexes, local state, and built + output remain excluded. Test servers are stopped. Graph evaluator/protocol + behavior is unchanged, so evaluator checks are not applicable to this phase. + +Phase 4 is recorded in this checkpoint on `codex/0.8.1`; nothing has been pushed. The next +implementation phase is the separately reviewed Windows hardening work. Pinned +Settings calibration, a clean enforcing release run, and real Windows coverage +remain required before release. + +## Phase 5 implementation boundary + +The first patch makes generic artifact reads/replacements exact by default. +Known canonical Team repositories explicitly retain portable LF revisions for +uniform CRLF checkouts. Mixed Team/Wiki recovery selects the right byte mode by +known path and namespace; Wiki Markdown and operation logs remain exact. Config +attestation permits checkout conversion only in the Git comparison and keeps +exact reread checks. Legacy Timeline IDs retain logical LF offsets without +weakening raw scan limits. Inbox escapes CR and literal backslashes only in the +signed display so CRLF Wiki updates and interrupted recovery remain usable. + +The second patch retains bigint device/inode identity through the four confirmed +Wiki boundaries: source reads, file application, audit append/rollback, and index +directory/lock ownership. The connected shared Team artifact reader and lock +code now capture full-width stats as well. Lock initialization failures and +owner-read races must preserve replacement files. Numeric byte-count bounds, +lock JSON formats, public exports, and graph extraction/hashing are unchanged. +This is a bounded correction to those ownership chains, not a claim that every +filesystem path in Graph, Wiki adapters, or the installer has been redesigned. + +The existing `windows-2022`/`macos-14`, Node 22.22.0 portability job now runs 17 +affected Team/Wiki suites sequentially, followed by the packaged-install smoke. +The draft PR's `storage-portability` checks provide the actual platform results; +the user needs no Windows device. Settings heap calibration and other release +gates remain independent. + +## Phase 5 local verification + +- CRLF checks cover canonical Team reads/writes, config attestation and reread + races, exact Wiki approval/recovery, local receipt replacement, and stable + legacy Timeline IDs with raw-byte bounds preserved. +- Added 32 full-width identity regressions across shared artifacts and the four + Wiki ownership boundaries, including unchanged large identities, numeric + collisions, rollback, failed lock initialization, and foreign replacement + preservation. The focused storage suites passed locally. +- The broader root run passed 203 files and 3,316 tests, with two failures: + a stale architecture inventory and a combined Inbox terminal-state timeout. + The complete real Inbox suite passed on isolated rerun (60 tests). Its three + terminal states are now independent tests with the same assertions and + default timeout; all three pass. Architecture now pins the existing Relay + receipt cleanup and Phase 1's single staged grounding writer/rename; all 28 + architecture tests pass. Neither fix relaxes production safeguards. +- Production build and root/Hub workspace typechecks pass. The emitted public + declaration is byte-identical to the previous phase; dependencies, on-disk + schemas, and Hub assets are unchanged. Graph extraction/protocol code is + unchanged, so no new evaluator work is required by this phase. +- The packed fresh-install Project Hub and official agent-skills smoke passed, + including CLI/Hub reads, local preference persistence and stale-save checks, + and protection of existing project state. `git diff --check` passes; local + databases, receipts, and generated build output remain excluded. + +This verification does not substitute local results for Windows evidence or +claim that the first broad test invocation was entirely green. The draft PR +records runner results and any remaining release failures separately. + +## Draft PR integration + +PR #176 initially had a merge conflict with newer `main` fixes, preventing its +pull-request workflow from starting. Main through `faa6416` is incorporated into +the release branch; the only manual resolution combines its configurable +Graph-ignore import with Phase 1's bounded grounding-read import. The auto-merged +CLI retains both logging controls and upstream telemetry opt-out help. + +This integration also carries main's additive Graph build-result fields for +skipped files and declined compiler inputs. The earlier byte-identical public +declaration check applies to the isolated Phase 5 commits, before this main sync. +The Windows phase introduces no additional public API or schema change. + +The combined build and root/Hub typechecks pass, along with 111 focused tests +across nine grounding/setup/corpus/CLI/public API/architecture suites and all 85 +Graph/comparison evaluator tests. The expanded platform job and full root suite +remain enforced in the PR workflow. + +The first mergeable CI run, on `154ceb5`, passed both full Node 22/24 checks +(Node 22: 211 files, 3,353 tests plus 85 evaluator tests and package smoke) and +macOS portability (17 files, 336 tests, one platform-specific skip, build and +package smoke). Windows passed 334 tests and exposed two CRLF golden-fixture +comparisons plus a native directory-replacement fixture assumption. The fixes +pin capabilities goldens to LF and exercise full-width directory replacement +without requiring Windows to rename an open lock's parent; POSIX retains real +swap coverage. Production error mapping is unchanged. Two stale browser text +assertions are also corrected to the accepted Context/Relay behavior. + +Local rechecks pass: both goldens survive a real `core.autocrlf=true` Git +checkout unchanged, 19 capabilities tests, 13 Relay-preview tests, both affected +Playwright cases, and all typechecks. The draft PR's next checks verify these +test corrections on the actual runners. The performance gate remains blocked +by three missing Settings heap budgets; other first-pass runtime crossings are +unconfirmed because that immediate failure suppresses the confirmation run. +No performance budgets are widened by this checkpoint. + +On `f9bab38`, both full Node checks, all 424 Hub web tests, all 95 browser tests, +and macOS portability pass. Windows confirms the earlier fixture fixes, with +336 passing tests and one intermittent stale/repair lifecycle timeout: the +same case passed in 6.0 seconds previously and exceeded the 15-second test guard +on the slower rerun. A first allowance for those three lifecycle scenarios was +insufficient: the next Windows run passed them but timed out a different real +Inbox create case (previously 4.7/5.2 seconds, then 15.2 seconds). The final +correction sets a 30-second Windows hang guard for the one real Inbox test file; +Vitest restores runtime config after each file. The shared contract's special +case is removed. All assertions, other test files, non-Windows timeouts, +production limits, and pinned performance budgets remain unchanged. The four +affected local cases and all typechecks pass. Final Windows runner evidence is +attached to the draft PR checks. + +## Graph performance PR #180: initial CI and correction + +The separate graph performance branch targets `codex/0.8.1` through +[PR #180](https://github.com/mex-memory/mex/pull/180). Its +[implementation report](code-graph-performance-implementation.md) records the +scope and local measurements without changing the Phase 6 telemetry boundary. + +Initial CI run +[`34286120355`](https://github.com/mex-memory/mex/actions/runs/34286120355) +passed both Node 22/24 jobs and the Windows/macOS storage-portability jobs, +including the added graph lifecycle/storage coverage. The run was not green: +the browser job failed two outdated graph progress assertions, and final +performance enforcement failed because the three Settings heap budgets left +open in Phase 4 were still missing. The subsequent browser correction and +corrected-head CI verification are recorded below. + +The retained performance report completed schema-valid measurement on pinned +Ubuntu 24.04 x64 / Node 22.22.0. Its exact raw hash was validated before deriving +only the small/medium/large Settings heap limits: 6,302,493 / 6,304,420 / +6,308,933 bytes, each `ceil(p95 * 1.15)`. The +[calibration record](settings-heap-calibration.json) preserves runner, artifact, +commit, raw-sample, and hash evidence. Only those three previously absent leaves +and calibration-status metadata change; all existing Graph and other budgets +remain frozen. + +The deterministic missing-budget failure suppressed runtime confirmation. +Graph maintenance crossings in that first report remain unconfirmed and must +go through the ordinary enforcing/confirmation jobs after calibration. Final +correction tests and the next exact-head CI result are pending. The phase-local +verification and earlier calibration-pending notes above remain historical +checkpoint records; this update does not claim all CI is green or authorize a +merge or release. + +### Graph timing confirmation and accepted isolation cost + +Corrected run +[`34288560611`](https://github.com/mex-memory/mex/actions/runs/34288560611) +passed Node 22/24, browser, and Windows/macOS portability at `4d6683e`. +Independent pinned allocations confirmed exactly five material Graph timing +failures. The small fixtures expose fresh-process startup and compiler warmup; +the jobs finish successfully. No memory or other metric produced a final +material failure under the existing confirmation rules. + +The product decision accepts that latency for Hub responsiveness and release +of the graph child's memory after construction. Only small/medium Graph refresh +and small/medium/large Graph rebuild limits are recalibrated with the existing +formula from the first healthy corrected report. All other limits, fixtures, +sample counts, and enforcement rules stay unchanged. The +[timing calibration](graph-maintenance-timing-calibration.json) retains both +runner allocations, raw samples, old values, and unowned-budget proof. The +[local isolation diagnostic](graph-isolation-diagnostic.json) records the +separate memory tradeoff and does not supply calibration numbers. + +A clean enforcing run on the calibrated head remains a release gate. This work +continues only on PR #180 into `codex/0.8.1`; nothing is merged and telemetry +remains separate. + +## Scope limits + +- A small audited Relay follow-up is optional after core Relay fixes; broad + editing, reassignment, and reopening are deferred. +- Full Context Review, personalized Catch Up, semantic conflict detection, + organization-wide runtime/distribution, and a guided setup skill are deferred. +- New graph edges and attribution must come from actual records; missing or + unverified information stays visible as such. +- Git remains the sharing layer. A published working-tree artifact does not + prove commit, push, receipt, or another teammate's checkout state. + +## Verification and review + +Finish each phase with focused tests, typechecking, and a review of the changed +boundary. Build and browser-check Hub/package changes. Keep Windows byte +identity and inode changes separate. Run the complete relevant release checks +after integration; retain existing resource budgets and require the established +pinned Windows/Linux CI evidence before release where local checks cannot +provide it. Do not widen a budget merely to fit a change. + +Update this plan with actual results and remaining work as phases finish. + + +## Phase 6: usage telemetry and voluntary feedback + +This phase is isolated on `codex/0.8.1-telemetry` from `d64f171`, in draft +PR #188 targeting `codex/0.8.1`. The user explicitly approved a shared random +installation UUID across CLI and Hub, superseding the earlier no-IDs-in-Hub +plan, and subsequently approved restoring existing scaffold UUIDs for +shared-project estimates and adding configured AI-tool names. No project name, +repository remote, Member/artifact ID, email, or content enters events. Contact +collection remains voluntary in the hosted feedback form, without an analytics +identity in its URL. + +Distinct installations using a scaffold during 28 completed UTC days provide +a shared-project signal, not a verified team count: one person can use several +machines, and copied scaffold IDs or shared installations can distort it. +Configured tool names describe project setup rather than the current invoking +agent. Project config is read in a bounded, non-mutating snapshot without +minting identity; setup/init can reread at completion, while Hub changes appear +after restart. Someone with config access can link its UUID to that project. + +The implementation and validation record is in +[`telemetry-v2.md`](telemetry-v2.md). Network delivery stays best effort and +bounded. Historical local verification for `9cbfab8` passed: 3,500 root tests +(one skipped), 434 Hub web tests, two browser checks, workspace typecheck/build, +fresh packed-install smoke, and the actual-CLI latency benchmark. These results +do not validate the subsequent context additions. Their separate addendum +records 353 focused tests, typecheck/build, seven benchmark-harness tests and +the completed actual-CLI comparison. The metadata helper measured below 0.7 ms +at p95 locally; full-process differences were noisy and are retained without a +zero-overhead claim. Fresh full and platform CI is required before merge. See +the linked record for measurements and platform/release gates. The release +branch and main are not merged as part of this work. + +## Main sync after telemetry integration + +Telemetry PR #188 merged into `codex/0.8.1` as `0ad565d` after final head +`a37277b` passed every required check in +[run `34378404972`](https://github.com/mex-memory/mex/actions/runs/34378404972), +including Windows and release performance. This supersedes the Phase 6 pending +entries above; they remain records of earlier checkpoints. + +The user subsequently authorized bringing main through +`bc2d40a5b3db15fc27d9d2ee19ca642050bccc04` into the release branch. The 18 +upstream commits retain degraded Graph reads and extraction-relevant config +identity. The only textual conflicts were three catch blocks in `src/cli.ts`: +the resolution preserves upstream detailed maintenance diagnostics while using +`process.exitCode` and returning so release-branch telemetry can complete. +Hub reads retain strict freshness; the isolated Graph candidate still relies +on parent-owned validation and publication authority. Focused regressions also +cover oversized-gap rebuild and refresh through the worker boundary. + +Local integration verification before committing the main sync: + +1. **Public API:** Pass. The root declaration SHA is unchanged after the + integrated build; the conflict resolution adds no package-root exports. +2. **Read/write safety:** Focused checks pass. Upstream degraded CLI reads remain + bounded and explicit, Hub reads retain strict freshness, and parent-owned + candidate validation/publication remains intact. The real checkout's graph + database SHA is unchanged after validation. Full regression and rerun + evidence is recorded in item 4. +3. **Deterministic bounds:** Focused checks pass, including worker handling of + oversized graph gaps for rebuild and refresh. No resource budget is widened. + Fresh integrated release-performance CI: **pending**. +4. **Tests and typecheck:** All 257 focused tests across 16 files pass + (81.97 seconds), and root/Hub workspace typechecks pass. The full root run + completed 233 files in 657.63 seconds: 3,668 tests passed, one skipped, and + four hit the existing 15-second timeout. Those four unchanged Team/Inbox + cases all passed serially, in 2.64–7.52 seconds, with no timeout or code + adjustment. The first run overlapped evaluator tests at its beginning; + contention is plausible but not established solely by those durations. + This records the full run's failures rather than claiming a clean first run. +5. **Build and evaluators:** Full production build passes. Graph/comparison + evaluator tests pass: 38 Graph plus 47 comparison tests (85 total). +6. **Diff and sharing boundary:** `git diff --check`, staged diff checks, and + intended-path review pass. This integration and its context updates are + canonical working-tree changes; commit/push shares them on `codex/0.8.1`. + Generated indexes, local state, and build output remain excluded. +7. **Protocol and compatibility:** Focused tests pass for affected CLI/Graph + behavior, including failure diagnostics and worker maintenance. Regression + and evaluator results are recorded above; fresh integrated platform CI + remains **pending**. + +All planned feature phases are implemented. After integrated checks pass, the +remaining release-preparation checkpoint is to set the package and lockfile +version to 0.8.1 and prepare the changelog/release notes, including schema-v4 +team Relay compatibility and the approved telemetry boundary. The current +version and release notes still describe 0.8.0. Merging into main, tagging, and +publication require a separate release instruction. Optional/deferred features +in Scope limits do not become requirements for this checkpoint. + +### CI browser dependency setup correction + +Merge `465c192` passed both Node 22.22.0/24 full checks and Windows/macOS +storage/build/packed-install checks in run `34383592713`. Both browser and +release-performance setup failed before executing tests, including on a +failed-job retry: apt rejected a checksum mismatch from Google's +`chrome-stable/deb` repository. No performance measurements were produced. + +The exact [runner image installer](https://github.com/actions/runner-images/blob/ubuntu24/20260831.293/images/ubuntu/scripts/build/install-google-chrome.sh) +already removes `google-chrome.list`, but Chrome's +[installer template](https://github.com/chromium/chromium/blob/main/chrome/installer/linux/common/apt.include) +now creates `google-chrome.sources`. The browser job and both performance +attempts remove those two exact unused apt source files from their disposable +runners before the existing Playwright install command. All package checks, +browser versions, launch settings, visual baselines, and performance limits +remain unchanged. MEX runtime code is unaffected. + +For this CI-only follow-up: (1) public API unchanged; (2) application read/write +boundaries unchanged; (3) runtime bounds unchanged; (4) workflow YAML parsing +and Bash syntax checks pass for all three edited steps; (5) integrated build +and evaluator results above still apply to unchanged application code; +(6) diff checks pass and only workflow/context documentation is included; +(7) fresh Linux browser/performance execution remains required. Node/platform +CI also reruns on the new commit. These items supplement the seven-item +integration checklist above without treating setup failure as a test pass. + +## 0.8.1 release preparation + +The user authorized this checkpoint on 2026-09-10. Root `package.json`, its +two lockfile version fields, and the two capability goldens now use 0.8.1. +Private workspace and protocol/schema versions are unchanged. `flake.nix` +derives its version from the root package instead of a separate literal. + +The changelog groups all unreleased changes under 0.8.1, including the prior +FTS5, ignore-protection, and telemetry controls; 0.8.0 and earlier history is +byte-identical. Release notes and the English README cover Context, Inbox, +Relays, logging, grounding, Graph behavior, Windows, and telemetry. Upgrade +guidance includes skill preview/sync, restarting agent sessions, explicit Graph +maintenance, Relay-v4 team compatibility, and Node/FTS5 requirements. Translated +READMEs update install/upgrade guidance and explicitly retain their older +product-narrative notice. Documentation makes no claim that publication occurred. + +Local verification before committing: + +1. **Public API — pass.** `src/index.ts` is unchanged; the emitted declaration + SHA remains `dd0eb3f9260730c11064549bd7e90d18c42a2076faad9e8ec167fd18f738b496`. +2. **Read/write safety — pass.** No application mutation code changed. The + installed-package smoke verifies the existing fresh-setup, skill-install, + and Hub boundaries. The real checkout's Graph database SHA is unchanged. +3. **Deterministic bounds — pass.** Runtime budgets, dependency resolutions, + schemas, and protocol limits are untouched. The dependency lock differs only + in its two release-version fields. +4. **Tests and typecheck — pass.** All 82 tests in five version, capability, + built-CLI, public API, and shipped-skill suites pass, followed by root/Hub + typechecks. Fresh CI runs the complete suite on the preparation commit. +5. **Build and package — pass.** Production build, built CLI `--version` + (`0.8.1`), and `npm run test:hub:package` pass. The latter packs and installs + the local release in temporary consumer projects, covering assets, official + skills, version consistency, fresh setup, and installed Hub behavior. + Evaluator implementation and protocols are unchanged; the prior integrated + CI evidence remains in run `34385511415`. +6. **Diff and sharing boundary — pass.** Diff checks, version/lock consistency, + local document links, and historical changelog checks pass. Only release + metadata, documentation, the Nix version expression, and version goldens are + included; generated indexes, local state, and build output stay excluded. + Commit/push shares these canonical changes on `codex/0.8.1`. +7. **Compatibility — pass for the npm release.** Capability goldens retain + their existing shapes with the new package version. The guide documents + Relay v4, existing formats, opt-outs, and managed-instruction upgrades. + Nix build validation remains outstanding as explained below. + +The Nix flake's fixed dependency hash has survived since the original 0.3.2 +flake despite later dependency changes. The pinned fetcher includes exact +lockfile bytes in its output, so the release-version edit also affects that +hash. Neither Nix nor the standalone prefetch helper is available here; the +hash is not fabricated or marked verified. `COMPATIBILITY.md` records that Nix +hash regeneration and an actual build are still needed. This pre-existing +source-package issue is separate from the verified npm release path. + +After the preparation commit's CI passes, the remaining actions are merging +the reviewed PR into main, tagging its release commit, and publishing. Those +actions require a separate release instruction. The 0.8.1 changelog date is the +preparation date and should be aligned if publication happens on a later day. diff --git a/docs/design/code-graph-performance-implementation.md b/docs/design/code-graph-performance-implementation.md new file mode 100644 index 00000000..b0a42c63 --- /dev/null +++ b/docs/design/code-graph-performance-implementation.md @@ -0,0 +1,341 @@ +# Code graph performance implementation + +Status: implemented on `codex/0.8.1-graph-performance`, targeting `codex/0.8.1`. +This is branch work, not a published release. Telemetry remains separate. +The [resource investigation](code-graph-resource-investigation.md) records the +historical baseline and temporary experiments; its timings are not a matched +comparison with this final implementation. +The durable [performance results](code-graph-performance-results.json) retain +the final engine comparison, actual Hub report, and repeated-operation fixture +measurements summarized below. + +## Scope + +- **Fingerprint publication:** the engine calls the internal + `upsertFingerprintsInOwnedTransaction(db, entries)` only inside its complete + publication transaction. Omitting the nested corpus savepoint avoids the + measured SQLite memory-journal amplification. Ordinary `upsert` and + `upsertMany` retain independent batch rollback, including when an enclosing + caller catches a failure and continues. Duplicate entries still use the last + value; stable references, constraints, aliases, and unaffected rows survive. +- **Statement ownership:** GraphStore reuses a fixed set of synchronous hot + statements owned by that store and connection. Fingerprint point reads use + one fixed statement per connection through weak ownership. Dynamic queries + and caller iterators remain independent; there is no generic global SQL cache. +- **Construction working set:** continuity planning reads old IDs first. When + every ID survives, it avoids loading old node bodies and fingerprints. Moves + retain only planned aliases and the minimal fingerprints needed for fallback; + the old node/signature collections become collectible before publication. + Fresh nodes come from staging instead of a second database read. Resolved + compiler references become edges directly; unresolved/fallback references and + import bindings remain available for resolution. +- **Hub execution:** refresh/rebuild candidate construction runs in a disposable + Node process. The Hub retains the maintenance lease, validation, source and + database revalidation, and final atomic publication. Numeric parse counts and + phase updates reach Jobs without exposing source contents. Redundant nested + candidate hashes were removed while preserving the complete before/after byte + identity around validation and the final identity check before publication. + +The CLI keeps its in-process default. Changed-source refresh still stages and +rebuilds the full eligible corpus; this does not introduce incremental compiler +indexing, a language rewrite, a new graph schema, or a new public package API. + +## Safety and limits + +The owned fingerprint function requires **full outer rollback on every failure**. +An active transaction alone is insufficient authorization to use it. Code that +catches a write failure and continues must use the ordinary store methods. +Graph refresh does not renew tracked Markdown grounding fingerprints or +`bodyHash` baselines; existing explicit grounding review rules remain intact. + +The parent creates and identity-binds the child's temporary workspace. Child +results cannot authorize publication: they are bounded, validated messages, and +the parent checks the actual candidate after the child closes. Cancellation, +crash, malformed messages, early IPC disconnect, startup failure, and the build +hang guard stop construction without replacing the last trustworthy graph. +Cleanup waits for child `close`, then verifies workspace identity; it refuses to +remove a replaced directory. Full-width device/inode identities remain intact. + +A separate child watchdog reads an inherited parent-lifetime pipe while the +compiler thread is busy. Parent death closes that pipe and stops the child; +watchdog failure also stops construction. A fatal parent `SIGKILL` can still +leave its owned temporary workspace or candidate artifacts because parent +cleanup cannot run. Those artifacts are not published as a successful graph. + +The private message ceiling is 1 MiB. Ordinary progress updates are throttled +to four per second and respect IPC backpressure, with bounded phase/final-count +exceptions. Startup has a 15-second guard, construction a 30-minute hang guard, +and cancellation escalates termination after 500 ms. These are lifecycle bounds, +**not a CPU or peak-memory quota**. Compiler dependency loading can still create +a large transient working set. Process exit releases that child's address space; +it does not prove that every native, slow, or repository-specific leak is absent. + +Initial checks and parent validation/publication still perform synchronous work. +The Hub can pause at those boundaries, and cancellation there waits for the Hub +to handle the request. Cross-platform cleanup and lifecycle cases are included +in the existing macOS/Windows storage-portability matrix without changing its +pinned runners or Node version. The accepted five-leaf timing calibration is +recorded below; memory limits remain unchanged. Local macOS results do not +establish Windows behavior; platform CI remains required evidence. + +## Matched engine comparison + +Baseline and final production engines processed the same frozen MEX source +corpus and seed graph, in separate processes, baseline first. Both indexed all +723 files successfully and produced 33,479 nodes, 88,068 edges, and 23,680 +fingerprints. These instrumented local macOS arm64 runs used Node 22.17.1 and +SQLite 3.50.0. They measure the engine refresh, excluding the outer maintenance +service's candidate-copy/validation/publication and Hub costs. + +| Measurement | Baseline | Final production | +|---|---:|---:| +| Engine-reported refresh duration | 305,712 ms | 71,046 ms | +| Native statement preparations through sync return | 426,415 | 117 | +| Independently sampled peak RSS | 2,126.469 MiB | 1,681.328 MiB | +| JS heap after close and diagnostic GC | 47.710 MiB | 47.609 MiB | + +Refresh took about 77% less time in this pair, or about 4.3 times faster. The +sampled RSS peak was about 21% lower. This is one ordered local pair: compiler +GC, filesystem caches, instrumentation, compression, and host scheduling can +affect both results. It does not establish a portable speedup or memory budget, +and sampled RSS can miss brief peaks. The preparation count stops at sync +return; it excludes the subsequent unchanged refresh. + +Normalized output parity passed for all 12 data tables and four representative +ranked full-text queries. The comparison excludes operational timestamps and +surrogate row identifiers, retaining semantic snapshot provenance; both stores +passed SQLite integrity and foreign-key checks. This checks equivalent graph +meaning and retrieval output, not byte-identical database files. + +## Repeated-operation characterization + +A 180-file multiproject fixture performed five operations in one process, +including real source edits between unchanged checks. The results are retained +in the same [performance results](code-graph-performance-results.json). + +| Operation | Elapsed time | +|---|---:| +| Initial build | 2,234.621 ms | +| Unchanged refresh | 36.687 ms | +| Changed-source refresh | 1,995.934 ms | +| Second unchanged refresh | 31.941 ms | +| Second changed-source refresh | 2,007.576 ms | + +Every build/changed refresh indexed all 180 files; unchanged refreshes indexed +zero. Heap after engine close and three diagnostic GCs stayed between 40.09 and +41.68 MiB; the maximum sampled RSS was 509.47 MiB. This provides bounded +same-process retention evidence for this fixture, not a long soak or proof that +all leaks are absent. Forced GC is measurement-only and is not used by the +production graph path. + +## Actual Hub observation + +The local probe used the built Hub on MEX's repository with 723 parsed files, +made a source edit, completed refresh, then made another edit and cancelled the +next refresh during observed parsing. Environment: macOS arm64, Node 22.17.1, +SQLite 3.50.0. The Hub section of the durable +[performance results](code-graph-performance-results.json) was generated at +`2026-09-08T22:05:08.938Z`; CLI SHA-256 +`cad49214c1e692a5641a38a5503ba7005bfdce927afb14a2e4b39a71135e8fd8`. + +| Observation | Measured result | +|---|---:| +| Completed changed-source refresh | 92.107 s | +| Completed-run job polling p95 / maximum | 3.944 ms / 4.673 s | +| Completed-run session polling p95 / maximum | 2.796 ms / 4.673 s | +| Largest job poll across both runs | 5.238 s, during initial checks | +| Cancel HTTP acknowledgement during parsing | 8.036 ms | +| Cancel request to observed `interrupted` state | 135.598 ms | +| Peak sampled Hub + descendant RSS | 2,058,682,368 bytes (about 1,963 MiB) | +| Peak sampled RSS in observed publication phase, parent only | 335,282,176 bytes (about 320 MiB) | +| HTTP failures / requests across both runs | 0 / 1,489 | + +The completed refresh published a different graph and read back as fresh. The +cancelled run left the live graph byte-identical to the completed graph, kept +the second edit stale, stopped the child, and left no candidate files. Only the +Hub parent remained at the end of each run, and the probe stopped it afterward. + +Polling ran every 100 ms with at most one outstanding request per route, a +10-second HTTP deadline, and a 180-second job deadline. Busy-route ticks were +skipped rather than queued; p95 describes completed requests, so the maxima and +observed pauses matter. Validation/publication also produced multi-second +pauses. Phase attribution comes from client observations and can itself be +delayed by a blocked Hub. RSS sums concurrently observed processes and may count +shared pages twice; it is neither V8 live heap nor macOS physical footprint. +This single local run establishes Hub behavior, not a Hub speedup percentage or +a portable resource budget. The matched engine comparison above measures a +different scope. Pinned release results remain separate evidence. + +## Verification before PR #180 + +Focused regressions cover fingerprint output parity and full-width references, +foreign-key and partial bucket-write failures, catch-and-continue batch rollback, +outer publication rollback, statement reuse after rollback, iterator isolation, +unchanged-ID continuity without old-body/fingerprint reads, and rename/move alias +chains. Existing graph integrity and compiler tests retain ambiguity and output +checks. Candidate regressions cover process failure, cancellation, parent death, +workspace replacement, protocol failures, and bounded progress delivery. + +The root suite ran 216 files / 3,396 tests: 3,376 passed, one skipped, and 19 +initially failed. All 19 then passed on targeted reruns: eight 15-second Team +contract timeouts were rerun with one worker; ten process/listener permission +failures were rerun with the necessary sandbox permissions; one exact writer +inventory assertion was updated for identity-bound candidate workspace cleanup. +No timeout, safety detector, or release budget was relaxed. The final candidate +suite passed 12 tests in 6.98 seconds; the final focused integration rerun passed +56 tests. The full Hub web suite passed 428 tests across 18 files in 16.19 seconds. +Typecheck, build, and graph evaluation passed. Emitted package-root declarations +match the prior surface apart from comments. + +Final package smoke passed, including refresh/rebuild through the packed graph +entrypoint. The asset-only release gate passed. Rebuilt public declarations +again matched apart from comments, and the real checkout's graph SHA-256 stayed +unchanged. At this local checkpoint, Windows lifecycle and pinned runtime release +CI were still pending the PR; the local runs did not replace those gates. The +subsequent CI result and correction are recorded below. + +## Verify Checklist + +These are the seven exact items from `.mex/context/conventions.md`, with status +and evidence at the pre-PR local checkpoint. Current correction status follows. + +1. **The public `src/index.ts` surface and emitted declarations changed only if compatibility work explicitly requires it.** + **Pass:** `src/index.ts` is unchanged; emitted declarations differ only in + comments. The owned fingerprint writer stays outside the emitted class API. +2. **Ordinary reads remain non-mutating; writes have explicit authority, containment, revision, and failure-atomicity checks.** + **Pass:** immutable reads remain unchanged. Parent-held maintenance authority, + complete publication rollback, independent standalone batches, and exact + candidate/workspace identity checks retain their regression coverage. +3. **Inputs, scans, output, diagnostics, and retained local state remain deterministically bounded.** + **Qualified:** corpus, diagnostic, message, and progress bounds remain + enforced. There is no peak-RSS quota, and repeated fatal-parent exits can + leave owned temporary artifacts. Their total accumulation is not claimed to + be globally bounded. Windows lifecycle behavior still requires platform CI. +4. **Focused tests for the changed boundary pass, followed by `npm run typecheck`; run `npm test` without a concurrent build when full coverage is warranted.** + **Pass with recorded reruns:** root suite and targeted rerun results are + recorded above; typecheck passed. Final candidate and full Hub web suites + passed in sequential runs. +5. **Run `npm run build` for packaging/Hub/asset changes and `npm run eval:test` for graph evaluator or protocol changes.** + **Pass:** build, evaluation, packed refresh/rebuild smoke, and the asset-only + release gate passed. The local measurements above do not replace pinned + runtime release performance gates. +6. **`git diff --check` passes and only intended tracked paths changed; generated `.mex/*.db*`, `.mex/local/`, `dist/`, and unrelated worktree files remain unstaged.** + **Pass at review:** whitespace checks passed, generated state remained + unstaged, and the live graph SHA-256 stayed unchanged. Recheck the exact + staged file list before commit. +7. **Graph/Wiki protocol shapes, stable error codes, ordering, cursors, and non-mutation contracts remain covered when affected.** + **Pass:** existing protocol/integrity/evaluator tests, adapter/maintenance + regressions, full Hub web tests, and normalized graph/FTS parity cover these + boundaries. + +## PR #180 initial CI and correction, 2026-09-09 + +[CI run `34286120355`](https://github.com/mex-memory/mex/actions/runs/34286120355) +passed the Node 22/24 jobs and both Windows/macOS storage-portability jobs. This +supplies platform evidence for that tested head, including the new graph +lifecycle and storage suites. The workflow was not green: the browser job +failed two outdated graph progress assertions, and the final performance job +failed on three missing Settings heap budgets from Phase 4. The subsequent +browser correction and its CI verification are recorded below. + +The pinned Linux measurement completed and validated against the report schema, +but the deterministic `budget_missing` failure prevented runtime confirmation. +Its Graph maintenance crossings remain first-pass observations, not a confirmed +regression or a runtime pass. Existing Graph budgets remain unchanged. + +The Settings correction uses only the three missing heap leaves and +calibration-status metadata, with the existing `ceil(p95 * 1.15)` formula. Small, +medium, and large limits are 6,302,493, 6,304,420, and 6,308,933 bytes. The +[retained calibration record](settings-heap-calibration.json) identifies the +schema-valid Ubuntu 24.04 x64 / Node 22.22.0 report, all five raw samples, the +verified raw-report SHA-256, and the unchanged unowned-budget hash. All prior +asset/runtime limits, material thresholds, and confirmation rules remain +frozen. A clean enforcing CI result on the corrected head remains required. + +The correction also skips tree-sitter runtime initialization when there are no +uncached supported grammars to load. Explicit initialization, Python/Rust, and +compiler fallback retain their existing behavior. The benchmark now observes +the same job event stream as the Hub UI instead of making a status/database read +every 20 ms. Its report records that observation method; POST-to-terminal timing +and complete Hub/child resource sampling remain included. Stream frames, total +bytes, event count, and the original absolute deadline are bounded. + +Local correction verification passed: two affected browser cases, eight UI phase +cases, 48 grammar/extractor cases, 17 compiler/containment/Python/Rust engine cases, +70 release-gate/engine cases, and 70 event-stream/measurement/gate cases. Typecheck +and build passed. A real temporary Hub completed all four Graph/Wiki maintenance +kinds through the event-stream observer. These are integration checks, not new +calibration measurements. The earlier 723-file measurements describe `6e12e6d`; +this correction does not replace that evidence with unmeasured speedup claims. + +### Confirmed timing tradeoff and scoped calibration + +[Corrected CI run `34288560611`](https://github.com/mex-memory/mex/actions/runs/34288560611) +passed both Node versions, browser checks, and Windows/macOS portability at +`4d6683e`. Two independent pinned runners confirmed five material maintenance +timing failures: small/medium Graph refresh and small/medium/large rebuild. +No memory or other metric produced a final material failure, although some +repeated memory crossings remained advisory under the existing rules. + +The product decision accepts the small-job latency cost of a disposable worker +for Hub responsiveness and release of the compiler's address space afterward. +Only those five timing limits now use the first healthy corrected report's +`ceil(p95 * 1.15)` candidates: 1634/1689 ms for small refresh/rebuild, +1842/1820 ms for medium refresh/rebuild, and 2278 ms for large rebuild. +Large refresh and all memory, asset, read, Wiki, fixture, sample-count, and +confirmation constraints stay unchanged. The +[calibration record](graph-maintenance-timing-calibration.json) retains both +runner allocations and samples plus the prior values and unowned-budget hash. +A clean enforcing run on the new calibrated head remains required. + +Local calibration verification passed 46 release-budget tests, 14 CI +orchestration/workflow tests, 11 measurement tests, and all typechecks. Six +measurement cases initially hit sandbox process/listener restrictions and +passed when rerun with those permissions. Independent review verified the +exact five numeric changes, every retained sample/hash, and restoration of the +complete prior budget object. Both historical reports pass when replayed with +the accepted limits; that replay does not replace fresh enforcing CI. + +The [same-code local diagnostic](graph-isolation-diagnostic.json) ran two +warmups and three measured four-file rebuilds per execution mode, with the same +parent validation. Median elapsed time was 879.5 ms in a disposable process +versus 220.2 ms in a warm parent. Validation took about 19–21 ms in both modes; +process spawn to parsing took 490–494 ms in measured runs. All ten graph core +digests and counts matched. There was no fixed success-path cancellation wait. + +Isolation alone raised sampled aggregate peak RSS in that diagnostic from +320.6 to 403.7 MiB, while the surviving parent's final RSS fell from 320.5 to +164.0 MiB. The compiler process exists only during construction and exits before +parent validation/publication. Each process has its own runtime/heap; a child +watchdog thread observes parent death while compilation is synchronous. This +is a bounded Mac comparison in fixed mode order, not a universal peak-memory +reduction, pinned calibration, or proof about every possible memory leak. + +### Current Verify Checklist for the CI correction and calibration + +1. **The public `src/index.ts` surface and emitted declarations changed only if compatibility work explicitly requires it.** + **Pass:** no public exports or declaration shapes changed; the correction's + declaration comparison passed. Calibration changes only evidence and limits. +2. **Ordinary reads remain non-mutating; writes have explicit authority, containment, revision, and failure-atomicity checks.** + **Pass:** corrected-head Node/platform CI covered publication and rollback. + Calibration leaves those authorities and all production write paths intact. +3. **Inputs, scans, output, diagnostics, and retained local state remain deterministically bounded.** + **Qualified:** only the five explicitly accepted timing limits change after + independent confirmation. All memory and other limits remain guarded. No + peak-RSS quota or globally bounded fatal-parent artifact accumulation is + claimed. Corrected Windows/macOS suites pass; final enforcement is pending. +4. **Focused tests for the changed boundary pass, followed by `npm run typecheck`; run `npm test` without a concurrent build when full coverage is warranted.** + **Pass locally:** 71 focused budget, orchestration, workflow, and measurement + tests pass, followed by all typechecks. Corrected-head full Node suites pass; + final calibrated-head CI remains required. +5. **Run `npm run build` for packaging/Hub/asset changes and `npm run eval:test` for graph evaluator or protocol changes.** + **Pass for unchanged production:** corrected-head CI builds and evaluator + checks pass; this calibration changes no packaging or protocol. A clean + enforcing run with the accepted limits remains required. +6. **`git diff --check` passes and only intended tracked paths changed; generated `.mex/*.db*`, `.mex/local/`, `dist/`, and unrelated worktree files remain unstaged.** + **Pass at scoped review:** whitespace and exact five-leaf ownership checks + pass; only budgets/tests and retained documentation/evidence changed. Existing + grounding baselines and generated state remain unstaged and unchanged. +7. **Graph/Wiki protocol shapes, stable error codes, ordering, cursors, and non-mutation contracts remain covered when affected.** + **Pass:** corrected-head browser, compiler, and protocol checks pass. This + calibration changes no protocol, ranking, cursor, or read behavior. diff --git a/docs/design/code-graph-performance-results.json b/docs/design/code-graph-performance-results.json new file mode 100644 index 00000000..d320fd31 --- /dev/null +++ b/docs/design/code-graph-performance-results.json @@ -0,0 +1,1880 @@ +{ + "schemaVersion": 1, + "kind": "local-graph-performance-verification-not-release-budget", + "generatedAt": "2026-09-08T22:24:18.557Z", + "source": { + "baseBranch": "codex/0.8.1", + "baseCommit": "35bfc075888706d6b105b39b0d711ff7a588b551", + "implementationBranch": "codex/0.8.1-graph-performance", + "files": 723, + "seedGraphSha256": "0b6dd1f8e161f19e4a15111f125a83d2245f01c01e791f21e38f2c8a4a61af41" + }, + "environment": { + "os": "macOS 26.0.1", + "platform": "darwin", + "arch": "arm64", + "node": "v22.17.1", + "v8": "12.4.254.21-node.27", + "sqlite": "3.50.0", + "typescript": "5.9.3" + }, + "matchedEngineComparison": { + "method": { + "sameFrozenSourceAndInstalledDependencies": true, + "sameIndependentSeedCopy": true, + "order": [ + "baseline", + "production" + ], + "repetitions": 1, + "baseline": "Instrumented engine from the base commit, preserved by the resource investigation.", + "production": "Final engine implementation bundle; subsequent edits affected only process progress, tests, and documentation.", + "scope": "engine.sync and its internal publication transaction; excludes Hub maintenance copy, validation and atomic file replacement", + "nativePreparation": "Counts DatabaseSync.prepare calls, including diagnostic/connection startup work", + "externalRssSampleIntervalMs": 1000, + "diagnosticGc": "Three forced GCs 100ms apart after engine close; never a production GC policy", + "supervisorDeadlineSeconds": 600, + "supervisorSampledRssGuardMiB": 4608, + "telemetryDisabled": true, + "limitations": [ + "One sequential local pair, not a randomized or pinned-runner estimate. Host caching, compression, scheduling and instrumentation affect timings and RSS.", + "RSS is resident set size, not physical footprint or V8 live heap. Sampling can miss peaks; process lifetime max RSS is recorded separately.", + "Engine-only measurements exclude Hub parent overhead; use actualHub for combined resource observations." + ] + }, + "runs": { + "baseline": { + "engineBundleSha256": "ea161e06667f8c879c543f48775bbb8eaeffb19d435ef72f71f0d2048c86a18b", + "result": { + "filesIndexed": 723, + "nodesCreated": 33479, + "edgesCreated": 88068, + "health": { + "ok": 723, + "partial": 0, + "failed": 0 + }, + "durationMs": 305712 + }, + "syncAndCloseMs": 306141.91766700003, + "totalProcessWorkMs": 306945.795917, + "nativePreparesAtSyncReturn": 426415, + "nativePreparesIncludingUnchanged": 427165, + "cpuAtSyncReturnMs": 299349.417, + "sampledPeakRssBytes": 2229764096, + "rssSampleCount": 307, + "processLifetimeMaxRssBytes": 2232270848, + "postGcMemory": { + "heapUsed": 50027096, + "heapTotal": 93093888, + "rss": 1612546048, + "external": 36239690, + "arrayBuffers": 559012 + }, + "unchanged": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 283, + "elapsedMs": 283.1332499999553 + } + }, + "production": { + "engineBundleSha256": "730b719ef06bf5b782a271d4eaef6642ccf22fbcbd0d381121bbb7f7b4eae380", + "result": { + "filesIndexed": 723, + "nodesCreated": 33479, + "edgesCreated": 88068, + "health": { + "ok": 723, + "partial": 0, + "failed": 0 + }, + "durationMs": 71046 + }, + "syncAndCloseMs": 71424.51699999999, + "totalProcessWorkMs": 72307.769625, + "nativePreparesAtSyncReturn": 117, + "nativePreparesIncludingUnchanged": 144, + "cpuAtSyncReturnMs": 83937.832, + "sampledPeakRssBytes": 1763000320, + "rssSampleCount": 73, + "processLifetimeMaxRssBytes": 1782480896, + "postGcMemory": { + "heapUsed": 49921200, + "heapTotal": 92045312, + "rss": 875397120, + "external": 36239690, + "arrayBuffers": 559012 + }, + "unchanged": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 306, + "elapsedMs": 306.12812499998836 + } + } + }, + "parity": { + "allEqual": true, + "tableCount": 12, + "normalizedTables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 88068, + "sha256": "d01913ff25625d699514a1f436f8e4bf930821a2542dd9ccb0e6dc734915cd88" + }, + "files": { + "rows": 723, + "sha256": "0443de844e9b476418c6d4bcf3f62153868dd63c87bd448e7e432250b12c9c96" + }, + "import_bindings": { + "rows": 11231, + "sha256": "093845203504383cab13844e2c274a286320718c2ee98cc530237829c7575bb0" + }, + "lsh_buckets": { + "rows": 757760, + "sha256": "8e2312ef5bc83526de8aaeb874c1aa15f27c43b260faab34f3db9f2a98dd73ef" + }, + "node_aliases": { + "rows": 1707, + "sha256": "0457b6cae3c4056f9b7f49745b48fda05e9e8e469783a43d014121051d47fc6b" + }, + "node_fingerprints": { + "rows": 23680, + "sha256": "aa817aba04adf15d0f05fe0e2f0e7dd09dc5fdba493b438de2f5d13a13824f34" + }, + "nodes": { + "rows": 33479, + "sha256": "53a1e1a1e99d84d13b54ea155f23f77c8ac2638462b4122e48229b7913e0025d" + }, + "project_metadata": { + "rows": 9, + "sha256": "45f3b5c9950772a8b6e39688bef164bd76edae182427216c2cf54a7d7c405644" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4627, + "sha256": "c08acb6cf46ec3bf8f09eaa44112adacc718e15b7070b131069b1860eeca35d9" + }, + "unresolved_refs": { + "rows": 88413, + "sha256": "13530185dbdedd99c28d1e974b9d9c6ffee5ff7292350bd421212eca7af6d647" + } + }, + "excludedColumns": { + "nodes": [ + "updated_at" + ], + "edges": [ + "id" + ], + "files": [ + "indexed_at" + ], + "unresolved_refs": [ + "id" + ], + "node_aliases": [ + "created_at" + ], + "source_chunks": [ + "id" + ], + "schema_versions": [ + "applied_at" + ], + "project_metadata": [ + "updated_at" + ], + "node_fingerprints": [ + "ref" + ] + }, + "snapshotExcluded": [ + "indexedAt", + "lastSuccessfulIndexAt" + ], + "ftsQueries": [ + { + "query": "graph", + "nodeResults": 30, + "sourceResults": 30, + "normalizedResultSha256": "3e3adc420981adb786d15ddaac5f6dc27dbe4a217628ed1af011248f7ee55063" + }, + { + "query": "compiler", + "nodeResults": 30, + "sourceResults": 30, + "normalizedResultSha256": "5d9c548e91281e9f77365dbccb4fbf0a27407c2bba22bb13092b13756b5bf6da" + }, + { + "query": "relay", + "nodeResults": 30, + "sourceResults": 30, + "normalizedResultSha256": "b26f68daf1dfdc46325b2e4ba04c5c5394ed43acaa4f68db61fec5fdd2edb4ab" + }, + { + "query": "fingerprint", + "nodeResults": 30, + "sourceResults": 30, + "normalizedResultSha256": "64562514151f05b4041b10e92ceba19735188e8baa0beafa1fdc14261585976b" + } + ], + "checks": { + "final-baseline": { + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "foreignKeyViolations": [] + }, + "final-production": { + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "foreignKeyViolations": [] + } + } + } + }, + "actualHub": { + "schemaVersion": 1, + "probe": "mex-actual-hub-responsiveness", + "generatedAt": "2026-09-08T22:05:08.938Z", + "environment": { + "node": "v22.17.1", + "sqlite": "3.50.0", + "platform": "darwin", + "arch": "arm64" + }, + "measurement": { + "pollIntervalMs": 100, + "maxOutstandingPerRoute": 1, + "httpTimeoutMs": 10000, + "jobDeadlineMs": 180000, + "totalGuardMs": 300000, + "scope": "Hub and observed descendants", + "rss": "sum of concurrent resident sets; shared pages may be counted twice", + "cpu": "observed process lifetime deltas; exited final CPU and short-lived children can be missed", + "phaseAttribution": "Client-observed job phases. Outstanding request intervals survive phase changes. A blocked Hub can delay phase observation; start/end phase and overlapping observed phases do not establish exact internal phase timing." + }, + "checks": { + "beforeRefreshStale": true, + "successPublishedDifferentGraph": true, + "completedRefreshFresh": true, + "secondEditStale": true, + "cancelledChildCleanup": true, + "cancelPreservedLiveGraph": true, + "cancelledSourceRemainsStale": true, + "cancelLeftNoCandidateFiles": true + }, + "runs": [ + { + "kind": "changed_refresh_complete", + "elapsedMs": 92106.517, + "terminalState": "succeeded", + "cancelledDuringObservedParse": false, + "phases": [ + { + "phase": "queued", + "atMs": 0, + "pendingRequests": 0, + "oldestPendingMs": 0 + }, + { + "phase": "running", + "atMs": 3655.682, + "pendingRequests": 1, + "oldestPendingMs": 6.501 + }, + { + "phase": "stage", + "atMs": 8424.315, + "pendingRequests": 1, + "oldestPendingMs": 4673.206 + }, + { + "phase": "parse", + "atMs": 9696.769, + "pendingRequests": 1, + "oldestPendingMs": 2.637 + }, + { + "phase": "resolve", + "atMs": 57382.893, + "pendingRequests": 1, + "oldestPendingMs": 2.052 + }, + { + "phase": "validate", + "atMs": 80937.995, + "pendingRequests": 1, + "oldestPendingMs": 99.001 + }, + { + "phase": "publish", + "atMs": 88190.874, + "pendingRequests": 1, + "oldestPendingMs": 2818.593 + }, + { + "phase": "complete", + "atMs": 92088.212, + "pendingRequests": 1, + "oldestPendingMs": 73.151 + } + ], + "resources": { + "peakRssBytes": 2058682368, + "cpuMs": 110340, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "http": { + "requests": 1455, + "failures": 0, + "pendingAfterDrain": 0, + "skippedTicks": { + "session": 152, + "job": 152, + "create": 0, + "cancel": 0 + }, + "httpStatuses": { + "200": 1454, + "202": 1 + }, + "failureCodes": {}, + "overall": { + "session": { + "count": 727, + "p95Ms": 2.796, + "maxMs": 4673.005 + }, + "job": { + "count": 727, + "p95Ms": 3.944, + "maxMs": 4673.211 + }, + "create": { + "count": 1, + "p95Ms": 3642.652, + "maxMs": 3642.652 + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "byStartPhase": { + "queued": { + "session": { + "count": 1, + "p95Ms": 4.122, + "maxMs": 4.122 + }, + "job": { + "count": 1, + "p95Ms": 6.54, + "maxMs": 6.54 + }, + "create": { + "count": 1, + "p95Ms": 3642.652, + "maxMs": 3642.652 + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "running": { + "session": { + "count": 1, + "p95Ms": 4673.005, + "maxMs": 4673.005 + }, + "job": { + "count": 1, + "p95Ms": 4673.211, + "maxMs": 4673.211 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "stage": { + "session": { + "count": 13, + "p95Ms": 3.872, + "maxMs": 3.872 + }, + "job": { + "count": 13, + "p95Ms": 3.675, + "maxMs": 3.675 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "parse": { + "session": { + "count": 474, + "p95Ms": 2.826, + "maxMs": 6.984 + }, + "job": { + "count": 474, + "p95Ms": 3.935, + "maxMs": 19.934 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "resolve": { + "session": { + "count": 233, + "p95Ms": 1.6, + "maxMs": 98.375 + }, + "job": { + "count": 233, + "p95Ms": 3.755, + "maxMs": 99.128 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "validate": { + "session": { + "count": 3, + "p95Ms": 4270.511, + "maxMs": 4270.511 + }, + "job": { + "count": 3, + "p95Ms": 4271.106, + "maxMs": 4271.106 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "publish": { + "session": { + "count": 2, + "p95Ms": 3636.542, + "maxMs": 3636.542 + }, + "job": { + "count": 2, + "p95Ms": 3637.615, + "maxMs": 3637.615 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "complete": { + "session": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "job": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + } + }, + "byOverlappedPhase": { + "queued": { + "session": { + "count": 1, + "p95Ms": 4.122, + "maxMs": 4.122 + }, + "job": { + "count": 1, + "p95Ms": 6.54, + "maxMs": 6.54 + }, + "create": { + "count": 1, + "p95Ms": 3642.652, + "maxMs": 3642.652 + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "running": { + "session": { + "count": 1, + "p95Ms": 4673.005, + "maxMs": 4673.005 + }, + "job": { + "count": 2, + "p95Ms": 4673.211, + "maxMs": 4673.211 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "stage": { + "session": { + "count": 13, + "p95Ms": 3.872, + "maxMs": 3.872 + }, + "job": { + "count": 14, + "p95Ms": 4673.211, + "maxMs": 4673.211 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "parse": { + "session": { + "count": 474, + "p95Ms": 2.826, + "maxMs": 6.984 + }, + "job": { + "count": 475, + "p95Ms": 3.935, + "maxMs": 19.934 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "resolve": { + "session": { + "count": 233, + "p95Ms": 1.6, + "maxMs": 98.375 + }, + "job": { + "count": 234, + "p95Ms": 3.755, + "maxMs": 99.128 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "validate": { + "session": { + "count": 3, + "p95Ms": 4270.511, + "maxMs": 4270.511 + }, + "job": { + "count": 4, + "p95Ms": 4271.106, + "maxMs": 4271.106 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "publish": { + "session": { + "count": 2, + "p95Ms": 3636.542, + "maxMs": 3636.542 + }, + "job": { + "count": 3, + "p95Ms": 3637.615, + "maxMs": 3637.615 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "complete": { + "session": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "job": { + "count": 1, + "p95Ms": 73.189, + "maxMs": 73.189 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + } + }, + "slowestRequests": [ + { + "kind": "job", + "startMs": 3751.109, + "phaseAtStart": "running", + "httpStatus": 200, + "ok": true, + "finishMs": 8424.32, + "phaseAtEnd": "stage", + "latencyMs": 4673.211, + "observedPhasesOverlapped": [ + "running", + "stage" + ] + }, + { + "kind": "session", + "startMs": 3750.591, + "phaseAtStart": "running", + "httpStatus": 200, + "ok": true, + "finishMs": 8423.596, + "phaseAtEnd": "running", + "latencyMs": 4673.005, + "observedPhasesOverlapped": [ + "running" + ] + }, + { + "kind": "job", + "startMs": 81040.688, + "phaseAtStart": "validate", + "httpStatus": 200, + "ok": true, + "finishMs": 85311.794, + "phaseAtEnd": "validate", + "latencyMs": 4271.106, + "observedPhasesOverlapped": [ + "validate" + ] + }, + { + "kind": "session", + "startMs": 81040.484, + "phaseAtStart": "validate", + "httpStatus": 200, + "ok": true, + "finishMs": 85310.995, + "phaseAtEnd": "validate", + "latencyMs": 4270.511, + "observedPhasesOverlapped": [ + "validate" + ] + }, + { + "kind": "create", + "startMs": 5.979, + "phaseAtStart": "queued", + "httpStatus": 202, + "ok": true, + "finishMs": 3648.631, + "phaseAtEnd": "queued", + "latencyMs": 3642.652, + "observedPhasesOverlapped": [ + "queued" + ] + }, + { + "kind": "job", + "startMs": 88287.587, + "phaseAtStart": "publish", + "httpStatus": 200, + "ok": true, + "finishMs": 91925.202, + "phaseAtEnd": "publish", + "latencyMs": 3637.615, + "observedPhasesOverlapped": [ + "publish" + ] + }, + { + "kind": "session", + "startMs": 88287.399, + "phaseAtStart": "publish", + "httpStatus": 200, + "ok": true, + "finishMs": 91923.941, + "phaseAtEnd": "publish", + "latencyMs": 3636.542, + "observedPhasesOverlapped": [ + "publish" + ] + }, + { + "kind": "job", + "startMs": 85372.281, + "phaseAtStart": "validate", + "httpStatus": 200, + "ok": true, + "finishMs": 88190.94, + "phaseAtEnd": "publish", + "latencyMs": 2818.659, + "observedPhasesOverlapped": [ + "validate", + "publish" + ] + }, + { + "kind": "session", + "startMs": 85371.967, + "phaseAtStart": "validate", + "httpStatus": 200, + "ok": true, + "finishMs": 88190.049, + "phaseAtEnd": "validate", + "latencyMs": 2818.082, + "observedPhasesOverlapped": [ + "validate" + ] + }, + { + "kind": "job", + "startMs": 80838.994, + "phaseAtStart": "resolve", + "httpStatus": 200, + "ok": true, + "finishMs": 80938.122, + "phaseAtEnd": "validate", + "latencyMs": 99.128, + "observedPhasesOverlapped": [ + "resolve", + "validate" + ] + }, + { + "kind": "session", + "startMs": 80838.761, + "phaseAtStart": "resolve", + "httpStatus": 200, + "ok": true, + "finishMs": 80937.136, + "phaseAtEnd": "resolve", + "latencyMs": 98.375, + "observedPhasesOverlapped": [ + "resolve" + ] + }, + { + "kind": "job", + "startMs": 92015.061, + "phaseAtStart": "publish", + "httpStatus": 200, + "ok": true, + "finishMs": 92088.25, + "phaseAtEnd": "complete", + "latencyMs": 73.189, + "observedPhasesOverlapped": [ + "publish", + "complete" + ] + }, + { + "kind": "session", + "startMs": 92014.616, + "phaseAtStart": "publish", + "httpStatus": 200, + "ok": true, + "finishMs": 92087.637, + "phaseAtEnd": "publish", + "latencyMs": 73.021, + "observedPhasesOverlapped": [ + "publish" + ] + }, + { + "kind": "job", + "startMs": 28304.58, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 28324.514, + "phaseAtEnd": "parse", + "latencyMs": 19.934, + "observedPhasesOverlapped": [ + "parse" + ] + }, + { + "kind": "job", + "startMs": 28406.113, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 28420.848, + "phaseAtEnd": "parse", + "latencyMs": 14.735, + "observedPhasesOverlapped": [ + "parse" + ] + }, + { + "kind": "job", + "startMs": 80134.382, + "phaseAtStart": "resolve", + "httpStatus": 200, + "ok": true, + "finishMs": 80145.929, + "phaseAtEnd": "resolve", + "latencyMs": 11.547, + "observedPhasesOverlapped": [ + "resolve" + ] + }, + { + "kind": "job", + "startMs": 62629.201, + "phaseAtStart": "resolve", + "httpStatus": 200, + "ok": true, + "finishMs": 62638.383, + "phaseAtEnd": "resolve", + "latencyMs": 9.182, + "observedPhasesOverlapped": [ + "resolve" + ] + }, + { + "kind": "job", + "startMs": 64135.616, + "phaseAtStart": "resolve", + "httpStatus": 200, + "ok": true, + "finishMs": 64142.928, + "phaseAtEnd": "resolve", + "latencyMs": 7.312, + "observedPhasesOverlapped": [ + "resolve" + ] + }, + { + "kind": "session", + "startMs": 28405.688, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 28412.672, + "phaseAtEnd": "parse", + "latencyMs": 6.984, + "observedPhasesOverlapped": [ + "parse" + ] + }, + { + "kind": "job", + "startMs": 3649.181, + "phaseAtStart": "queued", + "httpStatus": 200, + "ok": true, + "finishMs": 3655.721, + "phaseAtEnd": "running", + "latencyMs": 6.54, + "observedPhasesOverlapped": [ + "queued", + "running" + ] + } + ] + }, + "resourcePhases": { + "running": { + "samples": 48, + "peakRssBytes": 404652032, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "stage": { + "samples": 12, + "peakRssBytes": 541917184, + "maxProcesses": 2, + "finalProcesses": 2 + }, + "parse": { + "samples": 474, + "peakRssBytes": 1976205312, + "maxProcesses": 2, + "finalProcesses": 2 + }, + "resolve": { + "samples": 234, + "peakRssBytes": 2058682368, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "validate": { + "samples": 73, + "peakRssBytes": 267010048, + "maxProcesses": 1, + "finalProcesses": 1 + }, + "publish": { + "samples": 38, + "peakRssBytes": 335282176, + "maxProcesses": 1, + "finalProcesses": 1 + } + } + }, + { + "kind": "changed_refresh_cancel", + "elapsedMs": 11232.437, + "terminalState": "interrupted", + "interruptedReason": "user_cancelled", + "cancelledDuringObservedParse": true, + "cancelRequestedAtMs": 10715.349, + "stateAt500MsGrace": "interrupted", + "phases": [ + { + "phase": "queued", + "atMs": 0, + "pendingRequests": 0, + "oldestPendingMs": 0 + }, + { + "phase": "running", + "atMs": 4478.008, + "pendingRequests": 1, + "oldestPendingMs": 4.963 + }, + { + "phase": "stage", + "atMs": 9812.965, + "pendingRequests": 1, + "oldestPendingMs": 5238.366 + }, + { + "phase": "parse", + "atMs": 10715.343, + "pendingRequests": 1, + "oldestPendingMs": 4.084 + }, + { + "phase": "interrupted", + "atMs": 10850.947, + "pendingRequests": 1, + "oldestPendingMs": 38.799 + } + ], + "resources": { + "peakRssBytes": 608321536, + "cpuMs": 10350, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "http": { + "requests": 34, + "failures": 0, + "pendingAfterDrain": 0, + "skippedTicks": { + "session": 52, + "job": 52, + "create": 0, + "cancel": 0 + }, + "httpStatuses": { + "200": 33, + "202": 1 + }, + "failureCodes": {}, + "overall": { + "session": { + "count": 16, + "p95Ms": 5236.74, + "maxMs": 5236.74 + }, + "job": { + "count": 16, + "p95Ms": 5238.374, + "maxMs": 5238.374 + }, + "create": { + "count": 1, + "p95Ms": 4457.835, + "maxMs": 4457.835 + }, + "cancel": { + "count": 1, + "p95Ms": 8.036, + "maxMs": 8.036 + } + }, + "byStartPhase": { + "queued": { + "session": { + "count": 1, + "p95Ms": 3.566, + "maxMs": 3.566 + }, + "job": { + "count": 1, + "p95Ms": 4.969, + "maxMs": 4.969 + }, + "create": { + "count": 1, + "p95Ms": 4457.835, + "maxMs": 4457.835 + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "running": { + "session": { + "count": 1, + "p95Ms": 5236.74, + "maxMs": 5236.74 + }, + "job": { + "count": 1, + "p95Ms": 5238.374, + "maxMs": 5238.374 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "stage": { + "session": { + "count": 9, + "p95Ms": 4.108, + "maxMs": 4.108 + }, + "job": { + "count": 9, + "p95Ms": 4.333, + "maxMs": 4.333 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "parse": { + "session": { + "count": 1, + "p95Ms": 38.79, + "maxMs": 38.79 + }, + "job": { + "count": 1, + "p95Ms": 38.803, + "maxMs": 38.803 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 1, + "p95Ms": 8.036, + "maxMs": 8.036 + } + }, + "interrupted": { + "session": { + "count": 4, + "p95Ms": 4.293, + "maxMs": 4.293 + }, + "job": { + "count": 4, + "p95Ms": 4.276, + "maxMs": 4.276 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + } + }, + "byOverlappedPhase": { + "queued": { + "session": { + "count": 1, + "p95Ms": 3.566, + "maxMs": 3.566 + }, + "job": { + "count": 1, + "p95Ms": 4.969, + "maxMs": 4.969 + }, + "create": { + "count": 1, + "p95Ms": 4457.835, + "maxMs": 4457.835 + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "running": { + "session": { + "count": 1, + "p95Ms": 5236.74, + "maxMs": 5236.74 + }, + "job": { + "count": 2, + "p95Ms": 5238.374, + "maxMs": 5238.374 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "stage": { + "session": { + "count": 9, + "p95Ms": 4.108, + "maxMs": 4.108 + }, + "job": { + "count": 10, + "p95Ms": 5238.374, + "maxMs": 5238.374 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + }, + "parse": { + "session": { + "count": 1, + "p95Ms": 38.79, + "maxMs": 38.79 + }, + "job": { + "count": 2, + "p95Ms": 38.803, + "maxMs": 38.803 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 1, + "p95Ms": 8.036, + "maxMs": 8.036 + } + }, + "interrupted": { + "session": { + "count": 4, + "p95Ms": 4.293, + "maxMs": 4.293 + }, + "job": { + "count": 5, + "p95Ms": 38.803, + "maxMs": 38.803 + }, + "create": { + "count": 0, + "p95Ms": null, + "maxMs": null + }, + "cancel": { + "count": 0, + "p95Ms": null, + "maxMs": null + } + } + }, + "slowestRequests": [ + { + "kind": "job", + "startMs": 4574.599, + "phaseAtStart": "running", + "httpStatus": 200, + "ok": true, + "finishMs": 9812.973, + "phaseAtEnd": "stage", + "latencyMs": 5238.374, + "observedPhasesOverlapped": [ + "running", + "stage" + ] + }, + { + "kind": "session", + "startMs": 4574.324, + "phaseAtStart": "running", + "httpStatus": 200, + "ok": true, + "finishMs": 9811.064, + "phaseAtEnd": "running", + "latencyMs": 5236.74, + "observedPhasesOverlapped": [ + "running" + ] + }, + { + "kind": "create", + "startMs": 15.015, + "phaseAtStart": "queued", + "httpStatus": 202, + "ok": true, + "finishMs": 4472.85, + "phaseAtEnd": "queued", + "latencyMs": 4457.835, + "observedPhasesOverlapped": [ + "queued" + ] + }, + { + "kind": "job", + "startMs": 10812.148, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 10850.951, + "phaseAtEnd": "interrupted", + "latencyMs": 38.803, + "observedPhasesOverlapped": [ + "parse", + "interrupted" + ] + }, + { + "kind": "session", + "startMs": 10811.585, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 10850.375, + "phaseAtEnd": "parse", + "latencyMs": 38.79, + "observedPhasesOverlapped": [ + "parse" + ] + }, + { + "kind": "cancel", + "startMs": 10715.355, + "phaseAtStart": "parse", + "httpStatus": 200, + "ok": true, + "finishMs": 10723.391, + "phaseAtEnd": "parse", + "latencyMs": 8.036, + "observedPhasesOverlapped": [ + "parse" + ] + }, + { + "kind": "job", + "startMs": 4473.045, + "phaseAtStart": "queued", + "httpStatus": 200, + "ok": true, + "finishMs": 4478.014, + "phaseAtEnd": "running", + "latencyMs": 4.969, + "observedPhasesOverlapped": [ + "queued", + "running" + ] + }, + { + "kind": "job", + "startMs": 10711.259, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10715.592, + "phaseAtEnd": "parse", + "latencyMs": 4.333, + "observedPhasesOverlapped": [ + "stage", + "parse" + ] + }, + { + "kind": "session", + "startMs": 11213.377, + "phaseAtStart": "interrupted", + "httpStatus": 200, + "ok": true, + "finishMs": 11217.67, + "phaseAtEnd": "interrupted", + "latencyMs": 4.293, + "observedPhasesOverlapped": [ + "interrupted" + ] + }, + { + "kind": "job", + "startMs": 11213.749, + "phaseAtStart": "interrupted", + "httpStatus": 200, + "ok": true, + "finishMs": 11218.025, + "phaseAtEnd": "interrupted", + "latencyMs": 4.276, + "observedPhasesOverlapped": [ + "interrupted" + ] + }, + { + "kind": "session", + "startMs": 10711.059, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10715.167, + "phaseAtEnd": "stage", + "latencyMs": 4.108, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "job", + "startMs": 10309.264, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10313.213, + "phaseAtEnd": "stage", + "latencyMs": 3.949, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "session", + "startMs": 10308.813, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10312.744, + "phaseAtEnd": "stage", + "latencyMs": 3.931, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "session", + "startMs": 4472.858, + "phaseAtStart": "queued", + "httpStatus": 200, + "ok": true, + "finishMs": 4476.424, + "phaseAtEnd": "queued", + "latencyMs": 3.566, + "observedPhasesOverlapped": [ + "queued" + ] + }, + { + "kind": "session", + "startMs": 10510.483, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10513.782, + "phaseAtEnd": "stage", + "latencyMs": 3.299, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "job", + "startMs": 10510.714, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10513.981, + "phaseAtEnd": "stage", + "latencyMs": 3.267, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "session", + "startMs": 10108.567, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10111.251, + "phaseAtEnd": "stage", + "latencyMs": 2.684, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "job", + "startMs": 10108.802, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10111.379, + "phaseAtEnd": "stage", + "latencyMs": 2.577, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "session", + "startMs": 10207.996, + "phaseAtStart": "stage", + "httpStatus": 200, + "ok": true, + "finishMs": 10210.528, + "phaseAtEnd": "stage", + "latencyMs": 2.532, + "observedPhasesOverlapped": [ + "stage" + ] + }, + { + "kind": "session", + "startMs": 11113.506, + "phaseAtStart": "interrupted", + "httpStatus": 200, + "ok": true, + "finishMs": 11116.023, + "phaseAtEnd": "interrupted", + "latencyMs": 2.517, + "observedPhasesOverlapped": [ + "interrupted" + ] + } + ] + }, + "resourcePhases": { + "running": { + "samples": 53, + "peakRssBytes": 400998400, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "stage": { + "samples": 9, + "peakRssBytes": 588414976, + "maxProcesses": 2, + "finalProcesses": 2 + }, + "parse": { + "samples": 2, + "peakRssBytes": 608321536, + "maxProcesses": 2, + "finalProcesses": 1 + }, + "interrupted": { + "samples": 4, + "peakRssBytes": 394608640, + "maxProcesses": 1, + "finalProcesses": 1 + } + } + } + ], + "outcome": "passed", + "cliSha256": "cad49214c1e692a5641a38a5503ba7005bfdce927afb14a2e4b39a71135e8fd8", + "graphBefore": "0b6dd1f8e161f19e4a15111f125a83d2245f01c01e791f21e38f2c8a4a61af41", + "graphAfterCompleted": "3ba85d808f01bbffce5bf568b7735c49cc162380ba0f154d284cb92c6b06eb9b", + "cancelCleanupMs": 244.785, + "graphAfterCancelled": "3ba85d808f01bbffce5bf568b7735c49cc162380ba0f154d284cb92c6b06eb9b", + "cancelledGraphStatus": { + "indexStatus": "stale", + "changes": 1, + "parseTotal": 723, + "parseFailed": 0 + }, + "aggregateResources": { + "rssBytes": 394297344, + "peakRssBytes": 2058682368, + "cpuMs": 135770, + "maxProcesses": 2, + "sampleCount": 1319 + }, + "hubStopped": true, + "elapsedMs": 119951.455 + }, + "repeatedEngineCharacterization": { + "schemaVersion": 1, + "benchmark": "mex-graph-characterization", + "generatedAt": "2026-09-08T22:22:11.897Z", + "environment": { + "node": "v22.17.1", + "sqlite": "3.50.0", + "v8": "12.4.254.21-node.27", + "platform": "darwin", + "arch": "arm64" + }, + "workerBundleSha256": "1bff351b210a98339d416ff6abaa3e0da72610fa358d2969784871e1491442f3", + "fixture": { + "mutableSource": "packages/p0/module-0.ts", + "digest": "51b2557891202bc2b10d9e61cf93fa051ad0287a08d1be6888096bc00f875c4a", + "inputBytes": 465529, + "sourceFiles": 180, + "projects": 4, + "configFiles": 5, + "functionsPerFile": 12, + "inferredFiles": 20, + "dependencyDeclarations": 128, + "largeBodyLines": 4096 + }, + "measurement": { + "scope": "engine-worker-and-descendants", + "rss": "sum-of-resident-sets-in-one-sample", + "cpu": "observed-process-lifetime-deltas", + "intervalMs": 100, + "limitation": "Shared pages may be counted twice; short-lived children and final CPU between samples may be missed.", + "engineOnly": true, + "forcedGcAfterClose": 3, + "note": "Characterization only. Five operations share one process. CPU/RSS sampling is separate from post-GC retained memory; no laptop-derived release budgets." + }, + "samples": [ + { + "mode": "build", + "elapsedMs": 2234.6207080000004, + "cpuMs": 4422.221, + "result": { + "filesIndexed": 180, + "nodesCreated": 2120, + "edgesCreated": 4047, + "health": { + "ok": 180, + "partial": 0, + "failed": 0 + }, + "durationMs": 2227 + }, + "returnMemory": { + "rss": 502366208, + "heapTotal": 189759488, + "heapUsed": 65053568, + "external": 36811249, + "arrayBuffers": 1103922 + }, + "postGcMemory": { + "rss": 502153216, + "heapTotal": 80740352, + "heapUsed": 42033784, + "external": 35790381, + "arrayBuffers": 83014 + }, + "aggregate": { + "rssBytes": 502153216, + "peakRssBytes": 515850240, + "cpuMs": 4510, + "maxProcesses": 1, + "sampleCount": 30 + } + }, + { + "mode": "unchanged", + "elapsedMs": 36.687167000000045, + "cpuMs": 57.063, + "result": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 36 + }, + "returnMemory": { + "rss": 502185984, + "heapTotal": 80740352, + "heapUsed": 44131016, + "external": 35793474, + "arrayBuffers": 83035 + }, + "postGcMemory": { + "rss": 502185984, + "heapTotal": 80740352, + "heapUsed": 42080280, + "external": 35790381, + "arrayBuffers": 83014 + }, + "aggregate": { + "rssBytes": 502185984, + "peakRssBytes": 502185984, + "cpuMs": 120, + "maxProcesses": 1, + "sampleCount": 3 + } + }, + { + "mode": "changed", + "elapsedMs": 1995.9336249999997, + "cpuMs": 3894.464, + "result": { + "filesIndexed": 180, + "nodesCreated": 2120, + "edgesCreated": 4047, + "health": { + "ok": 180, + "partial": 0, + "failed": 0 + }, + "durationMs": 1990 + }, + "returnMemory": { + "rss": 509165568, + "heapTotal": 352452608, + "heapUsed": 318783736, + "external": 36411852, + "arrayBuffers": 704485 + }, + "postGcMemory": { + "rss": 489357312, + "heapTotal": 81641472, + "heapUsed": 43705904, + "external": 35790381, + "arrayBuffers": 83014 + }, + "aggregate": { + "rssBytes": 489357312, + "peakRssBytes": 534216704, + "cpuMs": 3960, + "maxProcesses": 1, + "sampleCount": 26 + } + }, + { + "mode": "unchanged", + "elapsedMs": 31.94133299999976, + "cpuMs": 51.422, + "result": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 32 + }, + "returnMemory": { + "rss": 489422848, + "heapTotal": 81641472, + "heapUsed": 45694072, + "external": 35793474, + "arrayBuffers": 83035 + }, + "postGcMemory": { + "rss": 489078784, + "heapTotal": 81575936, + "heapUsed": 43407048, + "external": 35790381, + "arrayBuffers": 83014 + }, + "aggregate": { + "rssBytes": 489078784, + "peakRssBytes": 489357312, + "cpuMs": 120, + "maxProcesses": 1, + "sampleCount": 3 + } + }, + { + "mode": "changed", + "elapsedMs": 2007.5755, + "cpuMs": 3928.895, + "result": { + "filesIndexed": 180, + "nodesCreated": 2120, + "edgesCreated": 4047, + "health": { + "ok": 180, + "partial": 0, + "failed": 0 + }, + "durationMs": 2000 + }, + "returnMemory": { + "rss": 521142272, + "heapTotal": 418152448, + "heapUsed": 378976544, + "external": 36186882, + "arrayBuffers": 483141 + }, + "postGcMemory": { + "rss": 493256704, + "heapTotal": 81690624, + "heapUsed": 42033976, + "external": 35786755, + "arrayBuffers": 83014 + }, + "aggregate": { + "rssBytes": 493256704, + "peakRssBytes": 521109504, + "cpuMs": 4000, + "maxProcesses": 1, + "sampleCount": 26 + } + } + ] + } +} diff --git a/docs/design/code-graph-resource-investigation.json b/docs/design/code-graph-resource-investigation.json new file mode 100644 index 00000000..33d71ce0 --- /dev/null +++ b/docs/design/code-graph-resource-investigation.json @@ -0,0 +1,2775 @@ +{ + "schemaVersion": 1, + "kind": "diagnostic-investigation-not-release-budget", + "date": "2026-09-09", + "source": { + "branch": "codex/0.8.1", + "head": "35bfc075888706d6b105b39b0d711ff7a588b551", + "sourceFiles": 708, + "compilerSourceBytes": 10591819, + "sourceCorpusDigest": "c2e8bdbbfd66963b9fdf549235105732d3424021af43de7a4f55cfca9094fd50", + "manifestHash": "a4f5457f66e5c393a9eda8711c3ff5a7fc198e3382a46f4eda82c323005e78e8" + }, + "environment": { + "os": "macOS 26.0.1", + "architecture": "arm64", + "node": "22.17.1", + "v8": "12.4.254.21-node.27", + "sqlite": "3.50.0", + "typescript": "5.9.3", + "webTreeSitter": "0.25.10" + }, + "method": { + "cpuProfilerIntervalMicroseconds": 2000, + "externalRssSampleIntervalMilliseconds": 1000, + "diagnosticGC": "three forced GCs, 100ms apart, after close", + "supervisorDurationSeconds": 600, + "supervisorSampledRssLimitMiB": 4608, + "engineOnly": true, + "liveHubRunning": false, + "telemetryDisabled": true + }, + "runs": { + "real-baseline": { + "variant": "baseline", + "repeatedFullWork": false, + "v8OldSpaceMiB": null, + "cycles": [ + { + "durationMs": 312942, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 312942 + }, + "nativePreparesCumulative": 399775, + "nativePreparesSincePreviousSync": 399775, + "returnMemory": { + "heapUsed": 2420390688, + "heapTotal": 2511323136, + "rss": 1092485120, + "external": 39085747, + "arrayBuffers": 3402528, + "maxRSSKiB": 2057904 + }, + "postGcMemory": { + "heapUsed": 54255424, + "heapTotal": 98074624, + "rss": 1221197824, + "external": 36242231, + "arrayBuffers": 559012, + "maxRSSKiB": 2057904 + } + } + ], + "noop": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 311 + }, + "processLifetime": { + "label": "real-baseline", + "dbPath": "/real-baseline.sqlite", + "elapsedMs": 314516.87429199996, + "memory": { + "rss": 1225916416, + "heapTotal": 97812480, + "heapUsed": 54370056, + "external": 36242231, + "arrayBuffers": 559012 + }, + "resourceUsage": { + "userCPUTime": 292068175, + "systemCPUTime": 16656055, + "maxRSS": 2057904, + "sharedMemorySize": 0, + "unsharedDataSize": 0, + "unsharedStackSize": 0, + "minorPageFault": 607416, + "majorPageFault": 666, + "swappedOut": 0, + "fsRead": 0, + "fsWrite": 1, + "ipcSent": 0, + "ipcReceived": 3, + "signalsCount": 124884, + "voluntaryContextSwitches": 125256, + "involuntaryContextSwitches": 922716 + }, + "prepares": 400510 + }, + "stages": [ + { + "stage": "process-start", + "elapsedMs": 0, + "heapUsedBytes": 4895408, + "rssBytes": 41795584, + "lifetimeMaxRssKiB": 45920, + "cpuMicroseconds": { + "user": 33432, + "system": 167716 + }, + "nativePrepares": 0, + "metadata": {} + }, + { + "stage": "module-loaded", + "elapsedMs": 203, + "heapUsedBytes": 37256136, + "rssBytes": 149471232, + "lifetimeMaxRssKiB": 145968, + "cpuMicroseconds": { + "user": 258423, + "system": 188897 + }, + "nativePrepares": 0, + "metadata": {} + }, + { + "stage": "discover-start", + "elapsedMs": 500, + "heapUsedBytes": 43250088, + "rssBytes": 176553984, + "lifetimeMaxRssKiB": 172416, + "cpuMicroseconds": { + "user": 407312, + "system": 241724 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "discover-end", + "elapsedMs": 624, + "heapUsedBytes": 57263680, + "rssBytes": 196411392, + "lifetimeMaxRssKiB": 191808, + "cpuMicroseconds": { + "user": 483121, + "system": 308929 + }, + "nativePrepares": 708, + "metadata": { + "files": 708 + } + }, + { + "stage": "compiler-start", + "elapsedMs": 659, + "heapUsedBytes": 56554400, + "rssBytes": 197410816, + "lifetimeMaxRssKiB": 192784, + "cpuMicroseconds": { + "user": 509868, + "system": 323183 + }, + "nativePrepares": 708, + "metadata": { + "sourceBytes": 10591819, + "inputFiles": 709 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 690, + "heapUsedBytes": 62094760, + "rssBytes": 213221376, + "lifetimeMaxRssKiB": 208224, + "cpuMicroseconds": { + "user": 549704, + "system": 325757 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "roots": 7 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 1233, + "heapUsedBytes": 188120728, + "rssBytes": 337248256, + "lifetimeMaxRssKiB": 331216, + "cpuMicroseconds": { + "user": 1606681, + "system": 401461 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "sourceFiles": 292 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 3826, + "heapUsedBytes": 561624088, + "rssBytes": 718209024, + "lifetimeMaxRssKiB": 701376, + "cpuMicroseconds": { + "user": 6489666, + "system": 638657 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "owned": 7 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 3827, + "heapUsedBytes": 561658672, + "rssBytes": 718225408, + "lifetimeMaxRssKiB": 701392, + "cpuMicroseconds": { + "user": 6490049, + "system": 638788 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "roots": 95 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 4687, + "heapUsedBytes": 299624376, + "rssBytes": 828981248, + "lifetimeMaxRssKiB": 813360, + "cpuMicroseconds": { + "user": 7946261, + "system": 777243 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "sourceFiles": 929 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 12663, + "heapUsedBytes": 707320320, + "rssBytes": 954056704, + "lifetimeMaxRssKiB": 937008, + "cpuMicroseconds": { + "user": 19884996, + "system": 1233250 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "owned": 96 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 12663, + "heapUsedBytes": 707357416, + "rssBytes": 954056704, + "lifetimeMaxRssKiB": 937008, + "cpuMicroseconds": { + "user": 19885128, + "system": 1233394 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "roots": 6 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 13013, + "heapUsedBytes": 826958528, + "rssBytes": 1035976704, + "lifetimeMaxRssKiB": 1011696, + "cpuMicroseconds": { + "user": 20415260, + "system": 1289658 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "sourceFiles": 289 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 13332, + "heapUsedBytes": 898200680, + "rssBytes": 1100578816, + "lifetimeMaxRssKiB": 1074784, + "cpuMicroseconds": { + "user": 21181835, + "system": 1336952 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "owned": 6 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 13332, + "heapUsedBytes": 898205928, + "rssBytes": 1100578816, + "lifetimeMaxRssKiB": 1074784, + "cpuMicroseconds": { + "user": 21182169, + "system": 1337092 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "roots": 442 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 14449, + "heapUsedBytes": 1223080000, + "rssBytes": 1425457152, + "lifetimeMaxRssKiB": 1392256, + "cpuMicroseconds": { + "user": 22564545, + "system": 1548575 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "sourceFiles": 1216 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 34250, + "heapUsedBytes": 1987876680, + "rssBytes": 1710768128, + "lifetimeMaxRssKiB": 1964592, + "cpuMicroseconds": { + "user": 46168548, + "system": 2782959 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "owned": 442 + } + }, + { + "stage": "compiler-end", + "elapsedMs": 46805, + "heapUsedBytes": 1970172176, + "rssBytes": 1769897984, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 61857190, + "system": 4121250 + }, + "nativePrepares": 708, + "metadata": { + "files": 701, + "projects": 5 + } + }, + { + "stage": "grammar-start", + "elapsedMs": 46806, + "heapUsedBytes": 1970864392, + "rssBytes": 1770913792, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 61858359, + "system": 4121624 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "grammar-end", + "elapsedMs": 46830, + "heapUsedBytes": 1961558592, + "rssBytes": 1775304704, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 61878934, + "system": 4129430 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "materialize-end", + "elapsedMs": 47206, + "heapUsedBytes": 2026791072, + "rssBytes": 1595162624, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 62254586, + "system": 4200857 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fallback-resolution-start", + "elapsedMs": 47206, + "heapUsedBytes": 2026795000, + "rssBytes": 1595162624, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 62254660, + "system": 4200936 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fallback-resolution-end", + "elapsedMs": 47310, + "heapUsedBytes": 2064475120, + "rssBytes": 1583710208, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 62365969, + "system": 4222332 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fingerprints-start", + "elapsedMs": 47344, + "heapUsedBytes": 2064352904, + "rssBytes": 1572552704, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 62405835, + "system": 4230245 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fingerprints-end", + "elapsedMs": 48744, + "heapUsedBytes": 2107445696, + "rssBytes": 1391083520, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 64021814, + "system": 4317565 + }, + "nativePrepares": 708, + "metadata": { + "fingerprints": 23376 + } + }, + { + "stage": "old-graph-start", + "elapsedMs": 48752, + "heapUsedBytes": 2109588712, + "rssBytes": 1389543424, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 64029008, + "system": 4319128 + }, + "nativePrepares": 1390, + "metadata": {} + }, + { + "stage": "old-graph-end", + "elapsedMs": 50731, + "heapUsedBytes": 2202368048, + "rssBytes": 1302118400, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 65253075, + "system": 4509759 + }, + "nativePrepares": 33535, + "metadata": { + "nodes": 32143, + "fingerprints": 22597 + } + }, + { + "stage": "clear-start", + "elapsedMs": 51018, + "heapUsedBytes": 2211790576, + "rssBytes": 1302200320, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 65346835, + "system": 4553306 + }, + "nativePrepares": 33535, + "metadata": {} + }, + { + "stage": "clear-end", + "elapsedMs": 54864, + "heapUsedBytes": 2211795536, + "rssBytes": 1169342464, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 66865127, + "system": 5720947 + }, + "nativePrepares": 33535, + "metadata": {} + }, + { + "stage": "source-chunks-end", + "elapsedMs": 56453, + "heapUsedBytes": 2213735816, + "rssBytes": 1157726208, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 68392404, + "system": 5819827 + }, + "nativePrepares": 44833, + "metadata": {} + }, + { + "stage": "insert-nodes-end", + "elapsedMs": 59605, + "heapUsedBytes": 2222031288, + "rssBytes": 1179418624, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 70932386, + "system": 6152929 + }, + "nativePrepares": 77954, + "metadata": {} + }, + { + "stage": "insert-edges-end", + "elapsedMs": 68483, + "heapUsedBytes": 2229366176, + "rssBytes": 1261764608, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 77199148, + "system": 7561833 + }, + "nativePrepares": 350266, + "metadata": { + "edges": 87048 + } + }, + { + "stage": "insert-fingerprints-end", + "elapsedMs": 306868, + "heapUsedBytes": 2227539120, + "rssBytes": 938196992, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 288646831, + "system": 15510549 + }, + "nativePrepares": 350316, + "metadata": {} + }, + { + "stage": "aliases-end", + "elapsedMs": 310671, + "heapUsedBytes": 2419725568, + "rssBytes": 1283719168, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291493233, + "system": 15890074 + }, + "nativePrepares": 399764, + "metadata": {} + }, + { + "stage": "fts-end", + "elapsedMs": 310882, + "heapUsedBytes": 2419729944, + "rssBytes": 1253113856, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291568295, + "system": 15917629 + }, + "nativePrepares": 399764, + "metadata": {} + }, + { + "stage": "invariants-end", + "elapsedMs": 311445, + "heapUsedBytes": 2419737776, + "rssBytes": 1205698560, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291631590, + "system": 15966388 + }, + "nativePrepares": 399767, + "metadata": {} + }, + { + "stage": "publication-end", + "elapsedMs": 313144, + "heapUsedBytes": 2419749936, + "rssBytes": 1083965440, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291694162, + "system": 16398730 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "sync-returned", + "elapsedMs": 313202, + "heapUsedBytes": 2420390688, + "rssBytes": 1092485120, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291697939, + "system": 16436343 + }, + "nativePrepares": 399775, + "metadata": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 312942 + } + }, + { + "stage": "engine-closed", + "elapsedMs": 313474, + "heapUsedBytes": 2420395464, + "rssBytes": 1092272128, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291699534, + "system": 16487541 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "after-forced-gc", + "elapsedMs": 313878, + "heapUsedBytes": 54255424, + "rssBytes": 1221197824, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291829537, + "system": 16589557 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "noop-returned", + "elapsedMs": 314190, + "heapUsedBytes": 66648368, + "rssBytes": 1226457088, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 291984142, + "system": 16645931 + }, + "nativePrepares": 400510, + "metadata": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 311 + } + }, + { + "stage": "after-noop-gc", + "elapsedMs": 314516, + "heapUsedBytes": 54354248, + "rssBytes": 1225916416, + "lifetimeMaxRssKiB": 2057904, + "cpuMicroseconds": { + "user": 292067874, + "system": 16655872 + }, + "nativePrepares": 400510, + "metadata": {} + } + ] + }, + "real-outer": { + "variant": "outer-owned fingerprint transaction", + "repeatedFullWork": false, + "v8OldSpaceMiB": null, + "cycles": [ + { + "durationMs": 99449, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 99449 + }, + "nativePreparesCumulative": 399775, + "nativePreparesSincePreviousSync": 399775, + "returnMemory": { + "heapUsed": 1517693248, + "heapTotal": 1956265984, + "rss": 606912512, + "external": 39365811, + "arrayBuffers": 3682592, + "maxRSSKiB": 1828608 + }, + "postGcMemory": { + "heapUsed": 52125632, + "heapTotal": 95502336, + "rss": 707641344, + "external": 36238605, + "arrayBuffers": 559012, + "maxRSSKiB": 1828608 + } + } + ], + "noop": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 394 + }, + "processLifetime": { + "label": "real-outer", + "dbPath": "/real-outer.sqlite", + "elapsedMs": 101308.909166, + "memory": { + "rss": 712998912, + "heapTotal": 95502336, + "heapUsed": 52243816, + "external": 36238605, + "arrayBuffers": 559012 + }, + "resourceUsage": { + "userCPUTime": 86888927, + "systemCPUTime": 13056038, + "maxRSS": 1828608, + "sharedMemorySize": 0, + "unsharedDataSize": 0, + "unsharedStackSize": 0, + "minorPageFault": 462936, + "majorPageFault": 274, + "swappedOut": 0, + "fsRead": 0, + "fsWrite": 0, + "ipcSent": 0, + "ipcReceived": 3, + "signalsCount": 41292, + "voluntaryContextSwitches": 86614, + "involuntaryContextSwitches": 580921 + }, + "prepares": 400510 + }, + "stages": [ + { + "stage": "process-start", + "elapsedMs": 0, + "heapUsedBytes": 4895472, + "rssBytes": 47267840, + "lifetimeMaxRssKiB": 46160, + "cpuMicroseconds": { + "user": 38394, + "system": 988076 + }, + "nativePrepares": 0, + "metadata": {} + }, + { + "stage": "module-loaded", + "elapsedMs": 201, + "heapUsedBytes": 37200432, + "rssBytes": 151863296, + "lifetimeMaxRssKiB": 148304, + "cpuMicroseconds": { + "user": 262748, + "system": 1006207 + }, + "nativePrepares": 0, + "metadata": {} + }, + { + "stage": "discover-start", + "elapsedMs": 547, + "heapUsedBytes": 41888264, + "rssBytes": 177979392, + "lifetimeMaxRssKiB": 173808, + "cpuMicroseconds": { + "user": 419447, + "system": 1063358 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "discover-end", + "elapsedMs": 760, + "heapUsedBytes": 56165112, + "rssBytes": 196116480, + "lifetimeMaxRssKiB": 191520, + "cpuMicroseconds": { + "user": 508119, + "system": 1144260 + }, + "nativePrepares": 708, + "metadata": { + "files": 708 + } + }, + { + "stage": "compiler-start", + "elapsedMs": 804, + "heapUsedBytes": 56802600, + "rssBytes": 197771264, + "lifetimeMaxRssKiB": 193136, + "cpuMicroseconds": { + "user": 536169, + "system": 1166719 + }, + "nativePrepares": 708, + "metadata": { + "sourceBytes": 10591819, + "inputFiles": 709 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 867, + "heapUsedBytes": 60381192, + "rssBytes": 213417984, + "lifetimeMaxRssKiB": 208416, + "cpuMicroseconds": { + "user": 578362, + "system": 1171409 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "roots": 7 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 1576, + "heapUsedBytes": 185618936, + "rssBytes": 348045312, + "lifetimeMaxRssKiB": 339888, + "cpuMicroseconds": { + "user": 1811123, + "system": 1298051 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "sourceFiles": 292 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 4783, + "heapUsedBytes": 554925760, + "rssBytes": 709722112, + "lifetimeMaxRssKiB": 693088, + "cpuMicroseconds": { + "user": 7761933, + "system": 1717155 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-contracts/tsconfig.json", + "owned": 7 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 4784, + "heapUsedBytes": 554991312, + "rssBytes": 709787648, + "lifetimeMaxRssKiB": 693152, + "cpuMicroseconds": { + "user": 7762275, + "system": 1717291 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "roots": 95 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 5762, + "heapUsedBytes": 769707168, + "rssBytes": 930414592, + "lifetimeMaxRssKiB": 908608, + "cpuMicroseconds": { + "user": 9286086, + "system": 1914296 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "sourceFiles": 929 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 16310, + "heapUsedBytes": 1287632112, + "rssBytes": 1342783488, + "lifetimeMaxRssKiB": 1373824, + "cpuMicroseconds": { + "user": 24380730, + "system": 2844307 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/hub-web/tsconfig.json", + "owned": 96 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 16310, + "heapUsedBytes": 1287637584, + "rssBytes": 1342783488, + "lifetimeMaxRssKiB": 1373824, + "cpuMicroseconds": { + "user": 24380841, + "system": 2844421 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "roots": 6 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 16759, + "heapUsedBytes": 1407701904, + "rssBytes": 1354547200, + "lifetimeMaxRssKiB": 1373824, + "cpuMicroseconds": { + "user": 24898050, + "system": 2943506 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "sourceFiles": 289 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 17087, + "heapUsedBytes": 1457494632, + "rssBytes": 1370292224, + "lifetimeMaxRssKiB": 1373824, + "cpuMicroseconds": { + "user": 25364709, + "system": 3008931 + }, + "nativePrepares": 708, + "metadata": { + "config": "/packages/mex-mcp/tsconfig.json", + "owned": 6 + } + }, + { + "stage": "ts-project-start", + "elapsedMs": 17088, + "heapUsedBytes": 1457498968, + "rssBytes": 1370292224, + "lifetimeMaxRssKiB": 1373824, + "cpuMicroseconds": { + "user": 25364796, + "system": 3009079 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "roots": 442 + } + }, + { + "stage": "ts-program-created", + "elapsedMs": 18252, + "heapUsedBytes": 440671704, + "rssBytes": 1440350208, + "lifetimeMaxRssKiB": 1406592, + "cpuMicroseconds": { + "user": 26969552, + "system": 3179551 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "sourceFiles": 1216 + } + }, + { + "stage": "ts-project-end", + "elapsedMs": 37452, + "heapUsedBytes": 1210058912, + "rssBytes": 1257455616, + "lifetimeMaxRssKiB": 1539600, + "cpuMicroseconds": { + "user": 50878058, + "system": 4147203 + }, + "nativePrepares": 708, + "metadata": { + "config": "/tsconfig.json", + "owned": 442 + } + }, + { + "stage": "compiler-end", + "elapsedMs": 48921, + "heapUsedBytes": 1068317480, + "rssBytes": 1806123008, + "lifetimeMaxRssKiB": 1778704, + "cpuMicroseconds": { + "user": 65351144, + "system": 5304210 + }, + "nativePrepares": 708, + "metadata": { + "files": 701, + "projects": 5 + } + }, + { + "stage": "grammar-start", + "elapsedMs": 48922, + "heapUsedBytes": 1069021176, + "rssBytes": 1806221312, + "lifetimeMaxRssKiB": 1778704, + "cpuMicroseconds": { + "user": 65352302, + "system": 5304366 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "grammar-end", + "elapsedMs": 48939, + "heapUsedBytes": 1062625496, + "rssBytes": 1809448960, + "lifetimeMaxRssKiB": 1778704, + "cpuMicroseconds": { + "user": 65368169, + "system": 5307426 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "materialize-end", + "elapsedMs": 49268, + "heapUsedBytes": 1128552944, + "rssBytes": 1824210944, + "lifetimeMaxRssKiB": 1782288, + "cpuMicroseconds": { + "user": 65743952, + "system": 5351962 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fallback-resolution-start", + "elapsedMs": 49268, + "heapUsedBytes": 1128556864, + "rssBytes": 1824210944, + "lifetimeMaxRssKiB": 1782288, + "cpuMicroseconds": { + "user": 65744026, + "system": 5352023 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fallback-resolution-end", + "elapsedMs": 49368, + "heapUsedBytes": 1164540256, + "rssBytes": 1836253184, + "lifetimeMaxRssKiB": 1804352, + "cpuMicroseconds": { + "user": 65862257, + "system": 5368141 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fingerprints-start", + "elapsedMs": 49398, + "heapUsedBytes": 1178104848, + "rssBytes": 1841774592, + "lifetimeMaxRssKiB": 1804352, + "cpuMicroseconds": { + "user": 65897094, + "system": 5370552 + }, + "nativePrepares": 708, + "metadata": {} + }, + { + "stage": "fingerprints-end", + "elapsedMs": 50783, + "heapUsedBytes": 1206845184, + "rssBytes": 1866874880, + "lifetimeMaxRssKiB": 1823120, + "cpuMicroseconds": { + "user": 67509844, + "system": 5435396 + }, + "nativePrepares": 708, + "metadata": { + "fingerprints": 23376 + } + }, + { + "stage": "old-graph-start", + "elapsedMs": 50791, + "heapUsedBytes": 1208991472, + "rssBytes": 1871626240, + "lifetimeMaxRssKiB": 1827760, + "cpuMicroseconds": { + "user": 67516956, + "system": 5436666 + }, + "nativePrepares": 1390, + "metadata": {} + }, + { + "stage": "old-graph-end", + "elapsedMs": 54004, + "heapUsedBytes": 1300395424, + "rssBytes": 1804451840, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 68797618, + "system": 5613765 + }, + "nativePrepares": 33535, + "metadata": { + "nodes": 32143, + "fingerprints": 22597 + } + }, + { + "stage": "clear-start", + "elapsedMs": 54302, + "heapUsedBytes": 1309906272, + "rssBytes": 1756643328, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 68893055, + "system": 5659608 + }, + "nativePrepares": 33535, + "metadata": {} + }, + { + "stage": "clear-end", + "elapsedMs": 65855, + "heapUsedBytes": 1309911392, + "rssBytes": 1005617152, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 70627231, + "system": 8119231 + }, + "nativePrepares": 33535, + "metadata": {} + }, + { + "stage": "source-chunks-end", + "elapsedMs": 67646, + "heapUsedBytes": 1313267752, + "rssBytes": 968933376, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 72142657, + "system": 8253877 + }, + "nativePrepares": 44833, + "metadata": {} + }, + { + "stage": "insert-nodes-end", + "elapsedMs": 71751, + "heapUsedBytes": 1321599880, + "rssBytes": 1073446912, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 74707253, + "system": 8708563 + }, + "nativePrepares": 77954, + "metadata": {} + }, + { + "stage": "insert-edges-end", + "elapsedMs": 87503, + "heapUsedBytes": 1327931528, + "rssBytes": 598540288, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 81391924, + "system": 10583306 + }, + "nativePrepares": 350266, + "metadata": { + "edges": 87048 + } + }, + { + "stage": "insert-fingerprints-end", + "elapsedMs": 91853, + "heapUsedBytes": 1326138216, + "rssBytes": 643776512, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 83550904, + "system": 11832649 + }, + "nativePrepares": 350316, + "metadata": {} + }, + { + "stage": "aliases-end", + "elapsedMs": 95727, + "heapUsedBytes": 1517018216, + "rssBytes": 855343104, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86296689, + "system": 12143422 + }, + "nativePrepares": 399764, + "metadata": {} + }, + { + "stage": "fts-end", + "elapsedMs": 96186, + "heapUsedBytes": 1517026392, + "rssBytes": 841351168, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86367884, + "system": 12174171 + }, + "nativePrepares": 399764, + "metadata": {} + }, + { + "stage": "invariants-end", + "elapsedMs": 96898, + "heapUsedBytes": 1517034208, + "rssBytes": 827834368, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86432019, + "system": 12221468 + }, + "nativePrepares": 399767, + "metadata": {} + }, + { + "stage": "publication-end", + "elapsedMs": 99648, + "heapUsedBytes": 1517046344, + "rssBytes": 596983808, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86498158, + "system": 12724562 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "sync-returned", + "elapsedMs": 99705, + "heapUsedBytes": 1517693248, + "rssBytes": 606912512, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86501184, + "system": 12759014 + }, + "nativePrepares": 399775, + "metadata": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 99449 + } + }, + { + "stage": "engine-closed", + "elapsedMs": 100208, + "heapUsedBytes": 1517698304, + "rssBytes": 612597760, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86503063, + "system": 12909110 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "after-forced-gc", + "elapsedMs": 100589, + "heapUsedBytes": 52125632, + "rssBytes": 707641344, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86632693, + "system": 12985786 + }, + "nativePrepares": 399775, + "metadata": {} + }, + { + "stage": "noop-returned", + "elapsedMs": 100984, + "heapUsedBytes": 64573400, + "rssBytes": 713375744, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86811938, + "system": 13046215 + }, + "nativePrepares": 400510, + "metadata": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 394 + } + }, + { + "stage": "after-noop-gc", + "elapsedMs": 101308, + "heapUsedBytes": 52219672, + "rssBytes": 712998912, + "lifetimeMaxRssKiB": 1828608, + "cpuMicroseconds": { + "user": 86888515, + "system": 13055857 + }, + "nativePrepares": 400510, + "metadata": {} + } + ] + }, + "real-outer-repeat": { + "variant": "outer-owned fingerprint transaction", + "repeatedFullWork": true, + "v8OldSpaceMiB": null, + "cycles": [ + { + "durationMs": 90237, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 90237 + }, + "nativePreparesCumulative": 399092, + "nativePreparesSincePreviousSync": 399092, + "returnMemory": { + "heapUsed": 1575282424, + "heapTotal": 1665564672, + "rss": 1246035968, + "external": 39554259, + "arrayBuffers": 3871040, + "maxRSSKiB": 1942112 + }, + "postGcMemory": { + "heapUsed": 54262416, + "heapTotal": 96239616, + "rss": 1298399232, + "external": 36242231, + "arrayBuffers": 559012, + "maxRSSKiB": 1942112 + } + }, + { + "durationMs": 79186, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 79186 + }, + "nativePreparesCumulative": 751207, + "nativePreparesSincePreviousSync": 352115, + "returnMemory": { + "heapUsed": 1565474288, + "heapTotal": 1635024896, + "rss": 998522880, + "external": 39008905, + "arrayBuffers": 3329312, + "maxRSSKiB": 1942112 + }, + "postGcMemory": { + "heapUsed": 54125696, + "heapTotal": 96862208, + "rss": 1060143104, + "external": 36238605, + "arrayBuffers": 559012, + "maxRSSKiB": 1942112 + } + }, + { + "durationMs": 82681, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 82681 + }, + "nativePreparesCumulative": 1103322, + "nativePreparesSincePreviousSync": 352115, + "returnMemory": { + "heapUsed": 442125376, + "heapTotal": 1652080640, + "rss": 1566867456, + "external": 36314761, + "arrayBuffers": 635168, + "maxRSSKiB": 2009120 + }, + "postGcMemory": { + "heapUsed": 55052568, + "heapTotal": 97910784, + "rss": 1544208384, + "external": 36238605, + "arrayBuffers": 559012, + "maxRSSKiB": 2009120 + } + } + ], + "noop": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 308 + }, + "processLifetime": { + "label": "real-outer-repeat", + "dbPath": "/real-outer-repeat.sqlite", + "elapsedMs": 255477.405042, + "memory": { + "rss": 1548910592, + "heapTotal": 97910784, + "heapUsed": 54985240, + "external": 36238605, + "arrayBuffers": 559012 + }, + "resourceUsage": { + "userCPUTime": 254841692, + "systemCPUTime": 29758297, + "maxRSS": 2009120, + "sharedMemorySize": 0, + "unsharedDataSize": 0, + "unsharedStackSize": 0, + "minorPageFault": 938710, + "majorPageFault": 281, + "swappedOut": 0, + "fsRead": 0, + "fsWrite": 0, + "ipcSent": 0, + "ipcReceived": 7, + "signalsCount": 103350, + "voluntaryContextSwitches": 160786, + "involuntaryContextSwitches": 1196418 + }, + "prepares": 1104057 + }, + "stages": [] + }, + "real-outer-stmtcache-repeat": { + "variant": "outer-owned fingerprint transaction; bounded statement reuse", + "repeatedFullWork": true, + "v8OldSpaceMiB": null, + "cycles": [ + { + "durationMs": 75620, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 75620 + }, + "nativePreparesCumulative": 42, + "nativePreparesSincePreviousSync": 42, + "returnMemory": { + "heapUsed": 2460181016, + "heapTotal": 2536325120, + "rss": 1989935104, + "external": 40030219, + "arrayBuffers": 4347000, + "maxRSSKiB": 2286192 + }, + "postGcMemory": { + "heapUsed": 54260880, + "heapTotal": 94928896, + "rss": 1952088064, + "external": 36242231, + "arrayBuffers": 559012, + "maxRSSKiB": 2286192 + } + }, + { + "durationMs": 67490, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 67490 + }, + "nativePreparesCumulative": 83, + "nativePreparesSincePreviousSync": 41, + "returnMemory": { + "heapUsed": 1469165896, + "heapTotal": 2019557376, + "rss": 1855848448, + "external": 38681225, + "arrayBuffers": 3001632, + "maxRSSKiB": 2344768 + }, + "postGcMemory": { + "heapUsed": 54118408, + "heapTotal": 100007936, + "rss": 1792851968, + "external": 36238605, + "arrayBuffers": 559012, + "maxRSSKiB": 2344768 + } + }, + { + "durationMs": 80050, + "result": { + "filesIndexed": 708, + "nodesCreated": 33121, + "edgesCreated": 87048, + "health": { + "ok": 708, + "partial": 0, + "failed": 0 + }, + "durationMs": 80050 + }, + "nativePreparesCumulative": 124, + "nativePreparesSincePreviousSync": 41, + "returnMemory": { + "heapUsed": 2417504904, + "heapTotal": 2506244096, + "rss": 1220722688, + "external": 39000713, + "arrayBuffers": 3321120, + "maxRSSKiB": 2582560 + }, + "postGcMemory": { + "heapUsed": 54943000, + "heapTotal": 97386496, + "rss": 1270890496, + "external": 36238605, + "arrayBuffers": 559012, + "maxRSSKiB": 2582560 + } + } + ], + "noop": { + "filesIndexed": 0, + "nodesCreated": 0, + "edgesCreated": 0, + "durationMs": 447 + }, + "processLifetime": { + "label": "real-outer-stmtcache-repeat", + "dbPath": "/real-outer-stmtcache-repeat.sqlite", + "elapsedMs": 227436.207041, + "memory": { + "rss": 1272430592, + "heapTotal": 97386496, + "heapUsed": 54875048, + "external": 36238605, + "arrayBuffers": 559012 + }, + "resourceUsage": { + "userCPUTime": 222677045, + "systemCPUTime": 26588721, + "maxRSS": 2582560, + "sharedMemorySize": 0, + "unsharedDataSize": 0, + "unsharedStackSize": 0, + "minorPageFault": 548096, + "majorPageFault": 271, + "swappedOut": 0, + "fsRead": 0, + "fsWrite": 0, + "ipcSent": 0, + "ipcReceived": 7, + "signalsCount": 86498, + "voluntaryContextSwitches": 130722, + "involuntaryContextSwitches": 897660 + }, + "prepares": 141 + }, + "stages": [] + }, + "real-outer-stmtcache-heap1024": { + "variant": "outer-owned transaction and bounded statement reuse", + "v8OldSpaceMiB": 1024, + "failure": { + "status": "failed", + "signal": "SIGABRT", + "pid": 66966, + "cause": "V8 heap exhaustion", + "proof": [ + "node::OOMErrorHandler", + "v8::internal::V8::FatalProcessOutOfMemory", + "v8::internal::Heap::FatalProcessOutOfMemory" + ], + "lastInstrumentedStage": "ts-program-created:root tsconfig", + "observedDurationMs": 48046, + "maxSampledRssKiB": 1299952, + "seedAndFailedDatabaseSha256": "0b6dd1f8e161f19e4a15111f125a83d2245f01c01e791f21e38f2c8a4a61af41", + "sourceSpoolAfterFatalExitBytes": 10594835, + "sourceSpoolCleanup": "removed exact newly created experiment spool; older unrelated spools left alone" + }, + "lastStage": { + "stage": "ts-program-created", + "elapsedMs": 17059, + "rss": 859701248, + "heapTotal": 474742784, + "heapUsed": 431751336, + "external": 2259388, + "arrayBuffers": 141707, + "maxRSSKiB": 1171936, + "cpu": { + "user": 28993085, + "system": 2321754 + }, + "prepares": 17, + "metadata": { + "config": "/tsconfig.json", + "sourceFiles": 1216 + } + } + } + }, + "limitations": [ + "Single workstation and real source corpus; profiler and host overhead; not calibrated release timings.", + "RSS, physical footprint, live heap and external memory are different overlapping metrics.", + "First repeated-series startup briefly overlapped the end of parity validation; use it for retention, not precise timing.", + "Repeated full work uses a nonempty source hint without changing actual source bytes.", + "Three GC observations per series cannot exclude native leaks or a slow retained-memory slope.", + "Fingerprint experiment omits nested savepoint only in a temporary bundle; production standalone API must retain atomicity.", + "Statement reuse experiment caches at most256native statements per database, clears on close; production iterators/reentrancy remain to be designed.", + "Initial supervisor depended on successful ps samples for deadline checks; later heap experiment independently checks deadline and escalates termination." + ], + "sqliteMicrobenchmarks": [ + { + "n": 1000, + "mode": "nested", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 56.41541600000001, + "cpuMs": 67.638, + "processPeakRSSBeforeValidation": 97040, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "1c9ea9d097db0b965be99f5b899b6a6beefbdc5642f4f724f2e364eef1824a9d" + }, + { + "n": 1000, + "mode": "outer", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 33.072708000000006, + "cpuMs": 43.442, + "processPeakRSSBeforeValidation": 83280, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "1c9ea9d097db0b965be99f5b899b6a6beefbdc5642f4f724f2e364eef1824a9d" + }, + { + "n": 1000, + "mode": "chunked", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 39.47562500000001, + "cpuMs": 50.065, + "processPeakRSSBeforeValidation": 88272, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "1c9ea9d097db0b965be99f5b899b6a6beefbdc5642f4f724f2e364eef1824a9d" + }, + { + "n": 1000, + "mode": "nested", + "tempStore": "FILE", + "fail": false, + "failure": null, + "writeMs": 173.860084, + "cpuMs": 149.595, + "processPeakRSSBeforeValidation": 82592, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "1c9ea9d097db0b965be99f5b899b6a6beefbdc5642f4f724f2e364eef1824a9d" + }, + { + "n": 2000, + "mode": "nested", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 268.033542, + "cpuMs": 280.795, + "processPeakRSSBeforeValidation": 146448, + "counts": { + "parent": 2000, + "buckets": 64000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "5ad06faa071e9c5c931a0f2ccc3598635ca2b574ed901e3ebbd5003e25511271" + }, + { + "n": 2000, + "mode": "outer", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 63.546583999999996, + "cpuMs": 84.881, + "processPeakRSSBeforeValidation": 111872, + "counts": { + "parent": 2000, + "buckets": 64000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "5ad06faa071e9c5c931a0f2ccc3598635ca2b574ed901e3ebbd5003e25511271" + }, + { + "n": 2000, + "mode": "chunked", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 85.14824999999999, + "cpuMs": 109.148, + "processPeakRSSBeforeValidation": 119424, + "counts": { + "parent": 2000, + "buckets": 64000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "5ad06faa071e9c5c931a0f2ccc3598635ca2b574ed901e3ebbd5003e25511271" + }, + { + "n": 2000, + "mode": "nested", + "tempStore": "FILE", + "fail": false, + "failure": null, + "writeMs": 270.42029199999996, + "cpuMs": 277.097, + "processPeakRSSBeforeValidation": 111392, + "counts": { + "parent": 2000, + "buckets": 64000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "5ad06faa071e9c5c931a0f2ccc3598635ca2b574ed901e3ebbd5003e25511271" + }, + { + "n": 4000, + "mode": "nested", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 1679.4848339999999, + "cpuMs": 1721.625, + "processPeakRSSBeforeValidation": 227728, + "counts": { + "parent": 4000, + "buckets": 128000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "e4e0df844494989c2361eaf5171c22a6cfacb3a07067af80cfc0ee085a47d47f" + }, + { + "n": 4000, + "mode": "outer", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 141.15587499999998, + "cpuMs": 184.957, + "processPeakRSSBeforeValidation": 161616, + "counts": { + "parent": 4000, + "buckets": 128000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "e4e0df844494989c2361eaf5171c22a6cfacb3a07067af80cfc0ee085a47d47f" + }, + { + "n": 4000, + "mode": "chunked", + "tempStore": "MEMORY", + "fail": false, + "failure": null, + "writeMs": 209.59279199999997, + "cpuMs": 253.153, + "processPeakRSSBeforeValidation": 170512, + "counts": { + "parent": 4000, + "buckets": 128000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "e4e0df844494989c2361eaf5171c22a6cfacb3a07067af80cfc0ee085a47d47f" + }, + { + "n": 4000, + "mode": "nested", + "tempStore": "FILE", + "fail": false, + "failure": null, + "writeMs": 564.145292, + "cpuMs": 612.788, + "processPeakRSSBeforeValidation": 161136, + "counts": { + "parent": 4000, + "buckets": 128000 + }, + "fkViolations": 0, + "rollbackExact": null, + "digest": "e4e0df844494989c2361eaf5171c22a6cfacb3a07067af80cfc0ee085a47d47f" + }, + { + "n": 1000, + "mode": "nested", + "tempStore": "MEMORY", + "fail": true, + "failure": "FOREIGN KEY constraint failed", + "writeMs": 16.521791000000007, + "cpuMs": 27.439, + "processPeakRSSBeforeValidation": 87776, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": true, + "digest": "1eb0fa708e16ec0d06388a444d10adc8d624f20a8527627095bb4bef3bf8a37b" + }, + { + "n": 1000, + "mode": "outer", + "tempStore": "MEMORY", + "fail": true, + "failure": "FOREIGN KEY constraint failed", + "writeMs": 12.76279199999999, + "cpuMs": 23.636, + "processPeakRSSBeforeValidation": 80768, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": true, + "digest": "1eb0fa708e16ec0d06388a444d10adc8d624f20a8527627095bb4bef3bf8a37b" + }, + { + "n": 1000, + "mode": "chunked", + "tempStore": "MEMORY", + "fail": true, + "failure": "FOREIGN KEY constraint failed", + "writeMs": 15.415667, + "cpuMs": 26.35, + "processPeakRSSBeforeValidation": 85760, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": true, + "digest": "1eb0fa708e16ec0d06388a444d10adc8d624f20a8527627095bb4bef3bf8a37b" + }, + { + "n": 1000, + "mode": "nested", + "tempStore": "FILE", + "fail": true, + "failure": "FOREIGN KEY constraint failed", + "writeMs": 50.757875, + "cpuMs": 59.243, + "processPeakRSSBeforeValidation": 80880, + "counts": { + "parent": 1000, + "buckets": 32000 + }, + "fkViolations": 0, + "rollbackExact": true, + "digest": "1eb0fa708e16ec0d06388a444d10adc8d624f20a8527627095bb4bef3bf8a37b" + } + ], + "parityChecks": [ + { + "labels": [ + "real-baseline", + "real-outer-repeat", + "real-outer-stmtcache-repeat" + ], + "allEqual": true, + "excludedColumns": { + "nodes": [ + "updated_at" + ], + "edges": [ + "id" + ], + "files": [ + "indexed_at" + ], + "unresolved_refs": [ + "id" + ], + "node_aliases": [ + "created_at" + ], + "source_chunks": [ + "id" + ], + "schema_versions": [ + "applied_at" + ], + "project_metadata": [ + "updated_at" + ], + "node_fingerprints": [ + "ref" + ] + }, + "snapshotExcluded": [ + "indexedAt", + "lastSuccessfulIndexAt" + ], + "results": { + "real-baseline": { + "tables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 87048, + "sha256": "f4d6dc24d3ba892aae8368cfa3a646c21df8e258dab5761779c850d55b879a61" + }, + "files": { + "rows": 708, + "sha256": "59653fd28a849882736b4a1f59f088d1d25e42bf898da3cbc3a7668daed2cd21" + }, + "import_bindings": { + "rows": 11062, + "sha256": "92de1ec993b07fdd838b4253255a848edce82d3263c2b804c27a3c9d7b68ed54" + }, + "lsh_buckets": { + "rows": 748032, + "sha256": "f87bd5290d4282281805e952f1c6ee49099a95f0f030885970a4537a95f4ade8" + }, + "node_aliases": { + "rows": 1465, + "sha256": "495f16e9b73f4fb8aa2f8b757e9d43da446b6df340b8ee9cff1de669b80308af" + }, + "node_fingerprints": { + "rows": 23376, + "sha256": "4014ca2ecc981b15f9800b32a8250dda83b6e90cfd056fa419c7ac5bfab03273" + }, + "nodes": { + "rows": 33121, + "sha256": "abb2cd9fe0a142766d249d3bc84aee12b3be52d68c52c6b63a16205622481501" + }, + "project_metadata": { + "rows": 9, + "sha256": "75c72d2d96d0284c399b1a7cba61be4665a10faee651612acc6ba31a1cc7a698" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4587, + "sha256": "ead9cc9cd850c4d20b6e2dd169927d27570eeb87f379da0954ca289b3a96587e" + }, + "unresolved_refs": { + "rows": 87154, + "sha256": "295ad1cfb16b0fd0a22aaf9542e77270d2156b827ef65d954c00e11dd848ea5d" + } + }, + "foreignKeyViolations": [], + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "fts": { + "graph": { + "nodes": 30, + "source": 30, + "sha256": "f5e96f006be23247cc7004e17b44b9093cd3255210075d0c1043bc5aadb470ba" + }, + "compiler": { + "nodes": 30, + "source": 30, + "sha256": "bcabe6b0ce020c5a8fa892430a89b09f3105c58e7bbcc4e892fa0d6900293082" + }, + "relay": { + "nodes": 30, + "source": 30, + "sha256": "2c58bea0e096e68446c2032249a55593505eea7b5e395d895d2f8b9d2727e287" + }, + "fingerprint": { + "nodes": 30, + "source": 30, + "sha256": "ee4552ac447f2dc476d0fc46e8e143a75c272e4bf32fdb5aacae9dc50b50e6f8" + } + } + }, + "real-outer-repeat": { + "tables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 87048, + "sha256": "f4d6dc24d3ba892aae8368cfa3a646c21df8e258dab5761779c850d55b879a61" + }, + "files": { + "rows": 708, + "sha256": "59653fd28a849882736b4a1f59f088d1d25e42bf898da3cbc3a7668daed2cd21" + }, + "import_bindings": { + "rows": 11062, + "sha256": "92de1ec993b07fdd838b4253255a848edce82d3263c2b804c27a3c9d7b68ed54" + }, + "lsh_buckets": { + "rows": 748032, + "sha256": "f87bd5290d4282281805e952f1c6ee49099a95f0f030885970a4537a95f4ade8" + }, + "node_aliases": { + "rows": 1465, + "sha256": "495f16e9b73f4fb8aa2f8b757e9d43da446b6df340b8ee9cff1de669b80308af" + }, + "node_fingerprints": { + "rows": 23376, + "sha256": "4014ca2ecc981b15f9800b32a8250dda83b6e90cfd056fa419c7ac5bfab03273" + }, + "nodes": { + "rows": 33121, + "sha256": "abb2cd9fe0a142766d249d3bc84aee12b3be52d68c52c6b63a16205622481501" + }, + "project_metadata": { + "rows": 9, + "sha256": "75c72d2d96d0284c399b1a7cba61be4665a10faee651612acc6ba31a1cc7a698" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4587, + "sha256": "ead9cc9cd850c4d20b6e2dd169927d27570eeb87f379da0954ca289b3a96587e" + }, + "unresolved_refs": { + "rows": 87154, + "sha256": "295ad1cfb16b0fd0a22aaf9542e77270d2156b827ef65d954c00e11dd848ea5d" + } + }, + "foreignKeyViolations": [], + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "fts": { + "graph": { + "nodes": 30, + "source": 30, + "sha256": "f5e96f006be23247cc7004e17b44b9093cd3255210075d0c1043bc5aadb470ba" + }, + "compiler": { + "nodes": 30, + "source": 30, + "sha256": "bcabe6b0ce020c5a8fa892430a89b09f3105c58e7bbcc4e892fa0d6900293082" + }, + "relay": { + "nodes": 30, + "source": 30, + "sha256": "2c58bea0e096e68446c2032249a55593505eea7b5e395d895d2f8b9d2727e287" + }, + "fingerprint": { + "nodes": 30, + "source": 30, + "sha256": "ee4552ac447f2dc476d0fc46e8e143a75c272e4bf32fdb5aacae9dc50b50e6f8" + } + } + }, + "real-outer-stmtcache-repeat": { + "tables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 87048, + "sha256": "f4d6dc24d3ba892aae8368cfa3a646c21df8e258dab5761779c850d55b879a61" + }, + "files": { + "rows": 708, + "sha256": "59653fd28a849882736b4a1f59f088d1d25e42bf898da3cbc3a7668daed2cd21" + }, + "import_bindings": { + "rows": 11062, + "sha256": "92de1ec993b07fdd838b4253255a848edce82d3263c2b804c27a3c9d7b68ed54" + }, + "lsh_buckets": { + "rows": 748032, + "sha256": "f87bd5290d4282281805e952f1c6ee49099a95f0f030885970a4537a95f4ade8" + }, + "node_aliases": { + "rows": 1465, + "sha256": "495f16e9b73f4fb8aa2f8b757e9d43da446b6df340b8ee9cff1de669b80308af" + }, + "node_fingerprints": { + "rows": 23376, + "sha256": "4014ca2ecc981b15f9800b32a8250dda83b6e90cfd056fa419c7ac5bfab03273" + }, + "nodes": { + "rows": 33121, + "sha256": "abb2cd9fe0a142766d249d3bc84aee12b3be52d68c52c6b63a16205622481501" + }, + "project_metadata": { + "rows": 9, + "sha256": "75c72d2d96d0284c399b1a7cba61be4665a10faee651612acc6ba31a1cc7a698" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4587, + "sha256": "ead9cc9cd850c4d20b6e2dd169927d27570eeb87f379da0954ca289b3a96587e" + }, + "unresolved_refs": { + "rows": 87154, + "sha256": "295ad1cfb16b0fd0a22aaf9542e77270d2156b827ef65d954c00e11dd848ea5d" + } + }, + "foreignKeyViolations": [], + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "fts": { + "graph": { + "nodes": 30, + "source": 30, + "sha256": "f5e96f006be23247cc7004e17b44b9093cd3255210075d0c1043bc5aadb470ba" + }, + "compiler": { + "nodes": 30, + "source": 30, + "sha256": "bcabe6b0ce020c5a8fa892430a89b09f3105c58e7bbcc4e892fa0d6900293082" + }, + "relay": { + "nodes": 30, + "source": 30, + "sha256": "2c58bea0e096e68446c2032249a55593505eea7b5e395d895d2f8b9d2727e287" + }, + "fingerprint": { + "nodes": 30, + "source": 30, + "sha256": "ee4552ac447f2dc476d0fc46e8e143a75c272e4bf32fdb5aacae9dc50b50e6f8" + } + } + } + } + }, + { + "labels": [ + "real-baseline", + "real-outer" + ], + "allEqual": true, + "excludedColumns": { + "nodes": [ + "updated_at" + ], + "edges": [ + "id" + ], + "files": [ + "indexed_at" + ], + "unresolved_refs": [ + "id" + ], + "node_aliases": [ + "created_at" + ], + "source_chunks": [ + "id" + ], + "schema_versions": [ + "applied_at" + ], + "project_metadata": [ + "updated_at" + ], + "node_fingerprints": [ + "ref" + ] + }, + "snapshotExcluded": [ + "indexedAt", + "lastSuccessfulIndexAt" + ], + "results": { + "real-baseline": { + "tables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 87048, + "sha256": "f4d6dc24d3ba892aae8368cfa3a646c21df8e258dab5761779c850d55b879a61" + }, + "files": { + "rows": 708, + "sha256": "59653fd28a849882736b4a1f59f088d1d25e42bf898da3cbc3a7668daed2cd21" + }, + "import_bindings": { + "rows": 11062, + "sha256": "92de1ec993b07fdd838b4253255a848edce82d3263c2b804c27a3c9d7b68ed54" + }, + "lsh_buckets": { + "rows": 748032, + "sha256": "f87bd5290d4282281805e952f1c6ee49099a95f0f030885970a4537a95f4ade8" + }, + "node_aliases": { + "rows": 1465, + "sha256": "495f16e9b73f4fb8aa2f8b757e9d43da446b6df340b8ee9cff1de669b80308af" + }, + "node_fingerprints": { + "rows": 23376, + "sha256": "4014ca2ecc981b15f9800b32a8250dda83b6e90cfd056fa419c7ac5bfab03273" + }, + "nodes": { + "rows": 33121, + "sha256": "abb2cd9fe0a142766d249d3bc84aee12b3be52d68c52c6b63a16205622481501" + }, + "project_metadata": { + "rows": 9, + "sha256": "75c72d2d96d0284c399b1a7cba61be4665a10faee651612acc6ba31a1cc7a698" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4587, + "sha256": "ead9cc9cd850c4d20b6e2dd169927d27570eeb87f379da0954ca289b3a96587e" + }, + "unresolved_refs": { + "rows": 87154, + "sha256": "295ad1cfb16b0fd0a22aaf9542e77270d2156b827ef65d954c00e11dd848ea5d" + } + }, + "foreignKeyViolations": [], + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "fts": { + "graph": { + "nodes": 30, + "source": 30, + "sha256": "f5e96f006be23247cc7004e17b44b9093cd3255210075d0c1043bc5aadb470ba" + }, + "compiler": { + "nodes": 30, + "source": 30, + "sha256": "bcabe6b0ce020c5a8fa892430a89b09f3105c58e7bbcc4e892fa0d6900293082" + }, + "relay": { + "nodes": 30, + "source": 30, + "sha256": "2c58bea0e096e68446c2032249a55593505eea7b5e395d895d2f8b9d2727e287" + }, + "fingerprint": { + "nodes": 30, + "source": 30, + "sha256": "ee4552ac447f2dc476d0fc46e8e143a75c272e4bf32fdb5aacae9dc50b50e6f8" + } + } + }, + "real-outer": { + "tables": { + "_mex_grounded_source": { + "rows": 13, + "sha256": "4799b3e1f4401c61c30b72a02cb99180821acf42d89152025109a9eabba8ed6e" + }, + "edges": { + "rows": 87048, + "sha256": "f4d6dc24d3ba892aae8368cfa3a646c21df8e258dab5761779c850d55b879a61" + }, + "files": { + "rows": 708, + "sha256": "59653fd28a849882736b4a1f59f088d1d25e42bf898da3cbc3a7668daed2cd21" + }, + "import_bindings": { + "rows": 11062, + "sha256": "92de1ec993b07fdd838b4253255a848edce82d3263c2b804c27a3c9d7b68ed54" + }, + "lsh_buckets": { + "rows": 748032, + "sha256": "f87bd5290d4282281805e952f1c6ee49099a95f0f030885970a4537a95f4ade8" + }, + "node_aliases": { + "rows": 1465, + "sha256": "495f16e9b73f4fb8aa2f8b757e9d43da446b6df340b8ee9cff1de669b80308af" + }, + "node_fingerprints": { + "rows": 23376, + "sha256": "4014ca2ecc981b15f9800b32a8250dda83b6e90cfd056fa419c7ac5bfab03273" + }, + "nodes": { + "rows": 33121, + "sha256": "abb2cd9fe0a142766d249d3bc84aee12b3be52d68c52c6b63a16205622481501" + }, + "project_metadata": { + "rows": 9, + "sha256": "75c72d2d96d0284c399b1a7cba61be4665a10faee651612acc6ba31a1cc7a698" + }, + "schema_versions": { + "rows": 4, + "sha256": "9ce76e9408211f3a56c2221a40c8a6624513dd0bd7214a1cbcb08375393c83f5" + }, + "source_chunks": { + "rows": 4587, + "sha256": "ead9cc9cd850c4d20b6e2dd169927d27570eeb87f379da0954ca289b3a96587e" + }, + "unresolved_refs": { + "rows": 87154, + "sha256": "295ad1cfb16b0fd0a22aaf9542e77270d2156b827ef65d954c00e11dd848ea5d" + } + }, + "foreignKeyViolations": [], + "quickCheck": [ + { + "quick_check": "ok" + } + ], + "fts": { + "graph": { + "nodes": 30, + "source": 30, + "sha256": "f5e96f006be23247cc7004e17b44b9093cd3255210075d0c1043bc5aadb470ba" + }, + "compiler": { + "nodes": 30, + "source": 30, + "sha256": "bcabe6b0ce020c5a8fa892430a89b09f3105c58e7bbcc4e892fa0d6900293082" + }, + "relay": { + "nodes": 30, + "source": 30, + "sha256": "2c58bea0e096e68446c2032249a55593505eea7b5e395d895d2f8b9d2727e287" + }, + "fingerprint": { + "nodes": 30, + "source": 30, + "sha256": "ee4552ac447f2dc476d0fc46e8e143a75c272e4bf32fdb5aacae9dc50b50e6f8" + } + } + } + } + } + ], + "nativeSample": { + "file": "baseline-insert.sample.txt", + "mainThreadSamples": 1513, + "memjrnlTruncateSamples": 1233, + "physicalFootprintAsReported": "3.3G", + "peakPhysicalFootprintAsReported": "3.4G", + "sha256": "ccd680c0df5a1f603e4db13cab3ed7fe58f895b89975acba9d794aea0288cf0a" + }, + "baselineCpuProfile": { + "file": "CPU.20260909.020015.49429.0.001.cpuprofile", + "sha256": "a7c3ddfa920cf20574f8a062d3e1a1824462fbfde99b06593ead02559541beff", + "sampledInclusiveSeconds": { + "captureCallReference": 23.46, + "callSignatureRendering": 14.986, + "callSignatureResolution": 6.46, + "parameterIsInvoked": 0.586 + }, + "summary": { + "totalMs": 315235.25, + "top": [ + { + "name": "run", + "url": "file:///engine-profile.mjs", + "line": 701, + "ms": 242393.20499995296, + "samples": 95804 + }, + { + "name": "(garbage collector)", + "url": "", + "line": 0, + "ms": 14117.451999999908, + "samples": 5744 + }, + { + "name": "exec", + "url": "file:///engine-profile.mjs", + "line": 710, + "ms": 4146.125, + "samples": 1647 + }, + { + "name": "DatabaseSync.prepare", + "url": "file:///run-profile.mjs", + "line": 13, + "ms": 4052.062, + "samples": 1627 + }, + { + "name": "", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54851, + "ms": 3332.497999999998, + "samples": 1371 + }, + { + "name": "transaction", + "url": "file:///engine-profile.mjs", + "line": 718, + "ms": 1711.2489999999996, + "samples": 659 + }, + { + "name": "getAccessibleSymbolChainFromSymbolTable", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54824, + "ms": 1533.9939999999979, + "samples": 634 + }, + { + "name": "forEachEntry", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 16325, + "ms": 1509.1239999999987, + "samples": 617 + }, + { + "name": "get", + "url": "file:///engine-profile.mjs", + "line": 705, + "ms": 1409.1299999999999, + "samples": 563 + }, + { + "name": "all", + "url": "file:///engine-profile.mjs", + "line": 706, + "ms": 1313.7079999999996, + "samples": 530 + }, + { + "name": "digest", + "url": "node:internal/crypto/hash", + "line": 125, + "ms": 1120.0000000000007, + "samples": 449 + }, + { + "name": "Hash", + "url": "node:internal/crypto/hash", + "line": 66, + "ms": 946.8290000000005, + "samples": 374 + }, + { + "name": "canonicalJson", + "url": "file:///engine-profile.mjs", + "line": 1612, + "ms": 723.122, + "samples": 291 + }, + { + "name": "copyFile", + "url": "", + "line": 0, + "ms": 692.5419999999999, + "samples": 141 + }, + { + "name": "getSymbolIfSameReference", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54631, + "ms": 663.4189999999999, + "samples": 282 + }, + { + "name": "readFileUtf8", + "url": "", + "line": 0, + "ms": 613.124, + "samples": 245 + }, + { + "name": "(idle)", + "url": "", + "line": 0, + "ms": 611.8329999999999, + "samples": 247 + }, + { + "name": "getAliasForSymbolInContainer", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54612, + "ms": 600.7570000000002, + "samples": 256 + }, + { + "name": "trySymbolTable", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54842, + "ms": 549.2949999999997, + "samples": 229 + }, + { + "name": "normalizedAbsolute", + "url": "file:///engine-profile.mjs", + "line": 5095, + "ms": 519.332, + "samples": 213 + }, + { + "name": "createFingerprintInternal", + "url": "file:///engine-profile.mjs", + "line": 408, + "ms": 434.82899999999967, + "samples": 178 + }, + { + "name": "scan", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 12794, + "ms": 423.335, + "samples": 205 + }, + { + "name": "read", + "url": "", + "line": 0, + "ms": 417.2509999999997, + "samples": 167 + }, + { + "name": "", + "url": "file:///engine-profile.mjs", + "line": 447, + "ms": 406.1150000000001, + "samples": 163 + }, + { + "name": "update", + "url": "node:internal/crypto/hash", + "line": 107, + "ms": 335.21000000000004, + "samples": 138 + }, + { + "name": "getAccessibleSymbolChain", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54805, + "ms": 334.13100000000003, + "samples": 142 + }, + { + "name": "resolveNameHelper", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 23574, + "ms": 331.8369999999999, + "samples": 159 + }, + { + "name": "forEachSymbolTableInScope", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54738, + "ms": 328.4509999999999, + "samples": 145 + }, + { + "name": "identifierFragmentComponents", + "url": "file:///engine-profile.mjs", + "line": 1458, + "ms": 313.455, + "samples": 127 + }, + { + "name": "skipTrivia", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 11781, + "ms": 309.67199999999997, + "samples": 133 + }, + { + "name": "getCandidateListForSymbol", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54872, + "ms": 291.95899999999995, + "samples": 124 + }, + { + "name": "resolve", + "url": "node:path", + "line": 1210, + "ms": 282.7919999999999, + "samples": 117 + }, + { + "name": "close", + "url": "file:///engine-profile.mjs", + "line": 729, + "ms": 274.792, + "samples": 3 + }, + { + "name": "intersectsRanges", + "url": "file:///engine-profile.mjs", + "line": 4871, + "ms": 267.329, + "samples": 109 + }, + { + "name": "resolveExternalModuleNameWorker", + "url": "file:///node_modules/typescript/lib/typescript.js", + "line": 54013, + "ms": 251.66899999999998, + "samples": 103 + } + ] + } + } +} diff --git a/docs/design/code-graph-resource-investigation.md b/docs/design/code-graph-resource-investigation.md new file mode 100644 index 00000000..f73bce06 --- /dev/null +++ b/docs/design/code-graph-resource-investigation.md @@ -0,0 +1,152 @@ +# Code graph resource investigation + +Investigation date: 2026-09-09 (Asia/Kolkata). Source: `codex/0.8.1` at `35bfc075888706d6b105b39b0d711ff7a588b551`. This is the historical investigation before implementation; the experiments below did not change production code. Implementation follows on `codex/0.8.1-graph-performance`, targeting the release branch. Telemetry remains separate. + +## Finding and recommendation + +A full refresh of MEX's own repository spent **238.4 of 312.9 seconds saving fingerprints**. Computing those fingerprints took only **1.4 seconds**. The dominant delay was SQLite's native savepoint/journal bookkeeping, not parsing or hashing. + +An isolated experiment that reused the graph publisher's existing outer transaction reduced fingerprint persistence to **4.35 seconds** and the full engine refresh to **99.45 seconds**. All 12 normalized data tables and four representative ranked full-text queries matched. Independent smaller experiments reproduced poor scaling and verified rollback on injected failures. These are local diagnostic measurements, not promised timings for every repository. + +Prioritize that small storage correction, then bounded statement ownership and the remaining compiler working set. Keep heavy graph maintenance off the Hub HTTP thread through a separately reviewed candidate-builder process. True incremental indexing and a language rewrite are larger projects; the evidence does not require them to fix the measured bottleneck. + +## What was measured + +The Hub was stopped before investigation. Its existing index remained untouched. A closed, quiescent copy of `.mex/graph.db` seeded each independent experiment under the OS temporary directory. The engine read the real repository and installed dependencies, staging its output into those temporary databases. No live Hub or network telemetry was used in the profiling runs. + +Environment: macOS 26.0.1 arm64; Node 22.17.1; V8 12.4.254.21-node.27; bundled SQLite 3.50.0; TypeScript 5.9.3; web-tree-sitter 0.25.10. The input was **708 source files**, with 10,591,819 bytes in the compiler input batch. Extraction produced **33,121 nodes, 87,048 edges, 23,376 fingerprints and 748,032 LSH buckets**; all 708 files parsed successfully. The seed index held 681 files and 32,143 nodes. + +The profiler bundled the current internal engine with stage markers, counted native statement preparations, captured V8 CPU samples every 2 ms, and independently sampled RSS/CPU once per second. An external supervisor watched for 600 seconds or 4,608 MiB sampled RSS. This is a diagnostic guard, not a strict OS memory quota. The original supervisor checked its deadline after successful `ps` calls; monitoring failure could therefore weaken that guard. Completed runs had successful samples and stayed below it. The final heap-budget experiment used an improved supervisor that enforced the deadline independently and escalated termination after two seconds. + +These measurements cover `engine.sync`, including its database transaction, but exclude the outer maintenance service's candidate-copy/validation/atomic-file-publication overhead and Hub/browser costs. Instrumentation has overhead. Runs were sequential; a parity check briefly overlapped the start of the first repeated-cycle series, so that series is retained-memory evidence rather than a calibrated timing comparison. macOS caching, compression, scheduling and I/O introduce variance. Release performance claims need unprofiled repetitions on the pinned runner. + +### Original and transaction experiment + +| Engine stage | Current code | Outer transaction experiment | +|---|---:|---:| +| Compiler extraction | 46.15 s | 48.12 s | +| Fingerprint computation | 1.40 s | 1.39 s | +| Fingerprint persistence | 238.39 s | 4.35 s | +| Full engine refresh | 312.94 s | 99.45 s | +| Subsequent unchanged refresh | 0.311 s | 0.394 s | +| Lifetime peak RSS | 2,010 MiB | 1,786 MiB | +| Heap after close and diagnostic GC | 51.7 MiB | 49.7 MiB | + +The fingerprint write was about **55 times faster**, and full refresh time fell about **68%**. Compiler work did not improve. Both runs made **400,510 native statement preparations**, including the subsequent no-op check. Do not attribute the entire modest RSS difference to the transaction change: compiler GC and host behavior varied. + +The baseline native sample also recorded **3.4 GB peak macOS physical footprint**. The earlier Hub observation recorded 3.5 GB peak physical footprint. Physical footprint, RSS and live V8 heap are different metrics; those numbers must not be mixed into a claimed memory reduction. Node documents the separate memory categories and lifetime peak semantics in its [process API](https://nodejs.org/download/release/v22.17.1/docs/api/process.html#processmemoryusage). + +## Why SQLite becomes slow + +The graph publication transaction already owns rollback for all derived rows. Inside it, [`FingerprintStore.upsertMany`](../../src/graph/fingerprint-store.ts) creates another savepoint covering the entire fingerprint corpus. [`configureConnection`](../../src/graph/db/database.ts) forces `temp_store=MEMORY`. + +The native sample during fingerprint insertion spent 1,233 of 1,513 main-thread samples in `memjrnlTruncate`, beneath `sqlite3PagerSavepoint`, statement completion and `sqlite3_step`. This is approximately 81.5% of that sample window, not of the entire refresh. The whole-run V8 profile attributed approximately 242 seconds to the SQLite `run` wrapper. + +The exact [SQLite 3.50.0 memory-journal implementation](https://github.com/sqlite/sqlite/blob/version-3.50.0/src/memjournal.c#L260) stores journal data in linked chunks. Truncating to a nonzero retained position walks from the head to that position. The [pager implementation](https://github.com/sqlite/sqlite/blob/version-3.50.0/src/pager.c) retains pages required by the outer user savepoint while releasing internal statement savepoints. MEX's temporary-store setting forces this subjournal to stay in RAM, disabling its usual spill threshold. Repeated writes can therefore repeatedly traverse a growing retained prefix. This source-backed mechanism matches both the sample and the scaling experiment. SQLite explains why [statement journals](https://www.sqlite.org/tempfiles.html#statement_journal_files) are needed for partial-statement rollback. + +The temporary full-repository variant removed only the four inner savepoint/rollback statements. Every insertion, constraint, graph transaction and graph output rule remained the same. That is an experiment, not the proposed public API: **generic fingerprint writes must keep independent batch atomicity**, including when a caller catches an error and continues its transaction. A production bulk-write entry point should explicitly belong to the graph publisher's owning transaction, with failures escaping to its rollback. Do not infer that ownership just from “a transaction exists.” + +### Independent SQLite scaling and failure checks + +Disk-backed WAL databases using MEX's fingerprint schema were seeded with prior data, then replaced using four transaction strategies. Entries had 32 buckets each. Values below are single-run fingerprint write times. + +| Fingerprints | Current nested savepoint, RAM journal | Outer transaction only | 250-entry savepoints | Nested savepoint, disk journal | +|---|---:|---:|---:|---:| +| 1,000 | 56 ms | 33 ms | 39 ms | 174 ms | +| 2,000 | 268 ms | 64 ms | 85 ms | 270 ms | +| 4,000 | 1,679 ms | 141 ms | 210 ms | 564 ms | + +Four times the input produced roughly 30 times the current write time, versus 4.3 times with outer ownership. At 4,000 entries, measured process peak before final validation was 222 MiB versus 158 MiB; those peaks include identical seeding and checksum warmup, not just the journal. + +All variants produced identical ordered fingerprint/bucket hashes and zero foreign-key violations. All four injected mid-write foreign-key failures restored the exact pre-write hash through the owning outer rollback. Smaller savepoints still rely on outer rollback for whole-batch atomicity. Disk temporary storage preserves the nested transaction structure and helps larger cases, but has an I/O cost and was slower than explicit outer ownership. It was not tested on the full repository. + +An exact-schema `EXPLAIN` also ruled out a proposed missing-index explanation for the parent fingerprint UPSERT: it does not scan `lsh_buckets`, and adding a `ref` index did not change that statement's plan. Bulk deletion of fingerprint/LSH rows before graph node deletion is already implemented. Neither should be advertised as a new fix. + +The relevant memory-journal and pager functions remain unchanged in the inspected upstream source. A newer SQLite version is worth compatibility testing, but upgrading alone is not an evidenced cure for this operation pattern. + +## Memory: what is established, and what is still uncertain + +A large transient working set is established. A persistent multi-gigabyte JavaScript leak is not established by these runs. + +After the baseline refresh, live JS heap fell from about 2,308 MiB to 52 MiB after engine close and three diagnostic GCs separated by event-loop turns. A repeated experiment ran three equivalent full-corpus refreshes in the **same process**, reopening and closing the engine each time. It used the transaction improvement, with a nonempty source hint to force full staging while leaving source bytes unchanged. This is repeated-work retention evidence; it is not an actual-edit/invalidation test. + +| Cycle | Full engine refresh | Post-GC heap | Post-GC RSS | +|---|---:|---:|---:| +| 1 | 90.24 s | 51.7 MiB | 1,238 MiB | +| 2 | 79.19 s | 51.6 MiB | 1,011 MiB | +| 3 | 82.68 s | 52.5 MiB | 1,473 MiB | + +The live heap returns to essentially the same level, while RSS remains high and variable. Three cycles cannot exclude a slow leak, native leak or repository-specific problem. They support collectible compiler/staging allocations rather than a retained multi-gigabyte JS graph. No native allocator dominator analysis or long soak was performed. Forced GC is solely a diagnostic tool, not the recommended production solution. + +A separate bounded statement-reuse experiment cached at most 256 native statements per database and cleared the cache on close. The first forced refresh made **42 preparations instead of 399,092**; three refreshes used 124 in total instead of 1,103,322. Their durations were 75.62, 67.49 and 80.05 seconds. Normalized graph data and the four FTS query sets matched the baseline after all three cycles. This supports reusable bulk statements as a compute improvement, although these are single series with host/GC variance. + +It did **not establish a memory improvement**: post-GC heap was 51.7, 51.6 and 52.4 MiB, post-GC RSS was 1,862, 1,710 and 1,212 MiB, and lifetime peak RSS reached 2,522 MiB. SQL allocation churn is real, but it does not explain away the compiler working set. The experimental generic cache is not a ready implementation: production reuse must account for concurrent iterators, statement settings, transaction boundaries and database lifetime. + +Finally, the optimized temporary engine was run with `--max-old-space-size=1024`. It **aborted from V8 heap exhaustion after about 48 seconds**, during root-project compiler extraction, before fingerprint persistence or graph publication. The macOS crash record for the exact child PID contained `node::OOMErrorHandler` and `V8::FatalProcessOutOfMemory`. Last sampled peak RSS was about 1,269 MiB; this was a failed experiment, not a successful 1 GiB operating target. + +The failed experiment database remained byte-identical to its seed (SHA-256 `0b6dd1f8e161f19e4a15111f125a83d2245f01c01e791f21e38f2c8a4a61af41`) with an empty WAL. A 10.6 MB source spool survived the fatal exit because `finally` cannot run after process abort; that exact experiment spool was removed. This adds a concrete process-isolation requirement: the supervisor must own or know the temporary workspace and clean it safely on crash/OOM. Merely setting a low heap cap or wrapping the current call in `try/catch` is not a solution. + +The source audit found concrete amplification that remains relevant: + +- Programs are processed sequentially, but each can load many dependency files. Declared project roots/loaded SourceFiles/owned graph files were 7/292/7, 95/929/96, 6/289/6 and 442/1,216/442. An inferred program processed the remaining compiler candidates. Dependencies outside the source corpus do not count against the compiler source-byte ledger. The 128 MiB compiler-source cap is not a RAM cap. +- Compiler programs and tree-sitter trees already have release boundaries. Captured compiler data is plain records; per-file WASM trees are deleted in `finally`. No obvious unbounded global AST owner was found. Recommending those existing releases again would not address the current cost. +- Publication eagerly loads all old nodes and fingerprints. Alias generation then reloads all fresh nodes and creates lookup maps while staged and old data are still live. Continuity logic usually accepts a surviving ID before it needs fingerprint matching; demand-driven old fingerprint reads and reuse of staged nodes are promising follow-ups that must preserve ambiguity and rename rules. +- Resolved compiler references are omitted from SQLite since PR #174, but an extra corresponding record is still retained in the staged heap. It may be removable after necessary resolution/validation, with fallback import hydration preserved. +- Rich strings have measurable size: signatures totaled 3.59 million characters, with one 272,441-character signature; edge evidence totaled 27.13 million characters, plus 7.22 million metadata characters. These are potential allocation and serialization costs. Truncating canonical signatures without a compatibility design can change identities or evidence. + +Compiler extraction remains the next major compute cost. Inclusive CPU-profile ancestry attributed approximately 23.5 seconds to call-reference capture, including 15.0 seconds rendering resolved call signatures and 6.5 seconds resolving signatures. These sampled inclusive values overlap and exclude GC attribution; do not sum them with wall-clock stage totals. A suspected repeated callback-body scan used only about 0.59 seconds. It is redundant work, but not a priority for this repository. + +## Why the Hub can look stuck + +[`src/hub/jobs/index.ts`](../../src/hub/jobs/index.ts) schedules the executor with a Promise microtask; [`graph.ts`](../../src/hub/jobs/graph.ts) directly invokes the repository graph adapter. The compiler and [Node `DatabaseSync`](https://nodejs.org/download/release/v22.17.1/docs/api/sqlite.html#class-databasesync) execute synchronously on that same thread. Declaring these methods async does not let the HTTP server run during the work. + +Current maintenance progress mostly reports discovery, staging, validation and publication. Cancellation checks occur at coarse engine hooks; a queued HTTP or IPC cancellation message cannot itself interrupt synchronous compiler/SQLite work. Existing tests cover mocked pending work and already-aborted signals, not responsive cancellation during a real compiler run. No new end-to-end HTTP latency measurement was taken in this investigation because the Hub remained stopped. + +The first mitigation improves time but leaves tens of seconds of same-thread work. A single disposable child process for candidate construction is justified for responsiveness, cancellation and releasing native/JS process state at exit. That boundary does not itself reduce total compute or peak aggregate memory. Avoid a worker pool or serializing complete graph objects; exchange a candidate path and bounded, coalesced stage/count messages. + +Prefer parent ownership of the existing repository lease, final source/identity validation and atomic publication. Keep expensive staging and candidate inspection off the HTTP thread where practical. A failed/killed child must leave the previous graph usable, with no orphan writer and no late publication after cancellation. Parent death, shutdown, OOM and Windows open-file/sidecar cleanup need real tests. [Worker resource limits](https://nodejs.org/download/release/v22.17.1/docs/api/worker_threads.html#new-workerfilename-options) constrain the JS engine, exclude external allocations and do not prevent whole-process OOM; a worker is not a hard native memory boundary. + +## How this relates to issue #140 + +The [original report](https://github.com/mex-memory/mex/issues/140) described MEX 0.7.2 on macOS arm64 with Python, JavaScript and Svelte: widely varying build times and a `check` reaching roughly 12 GB before being killed. In the [reporter's later 0.7.3 follow-up](https://github.com/mex-memory/mex/issues/140#issuecomment-5445828682), `check` completed in 3.3 seconds without memory ballooning and repair recovered the graph, but rebuilds still took 11m34s to more than 30 minutes. Those are the reporter's historical observations, not current reproduction timings. + +[PR #147](https://github.com/mex-memory/mex/pull/147) already fixed important read-path, compiler/WASM lifetime and fingerprint storage costs. [PR #174](https://github.com/mex-memory/mex/pull/174) already removed persisted resolved-reference duplication and added corpus exclusions. Both are ancestors of this branch. JavaScript without a tsconfig can still enter MEX's inferred TypeScript program, as verified in current source. + +The reporter's repository was not available locally. We cannot assign its remaining rebuild variance to the newly demonstrated savepoint path without a matching profile. We can say that current code still contains a severe, independently reproduced graph-build bottleneck and substantial memory amplification. + +## Proposed release work, in order + +1. **Storage correction — small and focused.** Add an explicit bulk path owned by the graph publisher's existing transaction; preserve generic fingerprint atomicity, duplicate handling, stable integer refs, constraints and every row value. Include exact-output parity and failures after both a fingerprint write and a bucket write, plus existing graph transaction/publication recovery tests. No schema or public protocol change is needed. +2. **Bound statement lifetime and reduce proven overlap — small to medium.** Reuse fixed prepared statements within a bulk operation/connection. Avoid a global cache; handle active iterators and close/reopen correctly. Then measure demand-driven continuity reads and staged-node reuse individually. Keep speculative compiler caches, identity changes and broad storage redesign outside this checkpoint. +3. **Isolate Hub graph construction — medium, separately reviewed.** One internal candidate-builder process, small progress protocol, supervised cancellation and safe failure. Preserve the graph adapter and publication contract. Plan this for 0.8.1 because the reported Hub stall remains after the storage fix; if it expands into a recovery redesign or cannot pass Windows/package tests, ship the storage correction independently and explicitly schedule isolation immediately afterward. Do not call the Hub issue fully fixed while heavy work still blocks it. +4. **Representative regression coverage alongside those changes.** Keep current budgets intact. Add a realistic multi-project/dependency fixture, repeated changed work, real HTTP/cancel responsiveness, and total Hub-plus-child CPU/RSS if isolation is introduced. Calibrate new gates on the pinned runner, not from this laptop. + +Do not put true dependency-aware incremental indexing, a persistent compiler daemon or a native engine rewrite into this small release. A nonempty supported-source change currently triggers whole-corpus staging to preserve cross-file semantic convergence; incremental updates must account for imports, exported types, moves/deletes, configuration, aliases and provenance. TypeScript's [builder API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#writing-an-incremental-program-watcher) does not maintain MEX's graph semantics automatically. The inspected [native TypeScript project](https://github.com/microsoft/typescript-go) lists its compiler API as not ready, so it is not a demonstrated drop-in replacement. The dominant work found here already runs in SQLite's native C. + +## Why the existing performance gate missed this + +The release fixtures contain **4, 16 and 48 tiny source modules**, one tsconfig, and no installed dependency graph. Their “large” designation means the largest synthetic fixture. The current 1,812 ms refresh budget and 604,061,696-byte refresh RSS ceiling are regression bounds for that fixture, not a resource promise for a 708-file dependency-heavy repository. + +Maintenance timing runs in one warmed Hub, but RSS is retained only for the first five of ten repetitions. The benchmark does not currently enforce stage CPU, post-cleanup retained-memory trends, concurrent HTTP latency or actual cancellation latency. Its sampler watches only the Hub PID: moving work to a child without changing that sampler would hide the cost. The polling loop also needs request timeouts so a blocked fetch cannot bypass its overall deadline. + +Add representative coverage without widening existing calibrated budgets. For storage, test clean build, unchanged refresh, meaningful changed refresh and failure rollback. For process isolation, verify real extraction-time HTTP responses, cancellation acknowledgment plus actual stop, child crash/OOM, parent shutdown and no late publish. Keep Windows/macOS/Linux packaging and ownership tests because process and temporary-file behavior differ. + +## Evidence retained and validation boundary + +[Measurement summary](code-graph-resource-investigation.json) retains runtime versions, source provenance, stage/memory observations, normalized table hashes, microbenchmark results and experiment limitations. Local raw CPU profiles, RSS JSONL, native samples, bundles and comparison scripts remain under `/tmp/mex-graph-investigation-20260909`; they were not added to Git. These contain repository paths/source-related profiler information and were not uploaded. + +To reproduce: check out the recorded commit, install matching dependencies, stop maintenance, verify the seed DB has no authoritative WAL, copy it to temporary storage, and bundle the internal engine outside the indexed corpus. Compare the same `sync` input and seed with only the targeted transform enabled. Record wall/CPU, memory categories, source/config/dependency identity and output digests. For retention, reopen/close the engine within one process and force equivalent full staging several times; use diagnostic GC only in the profiler. For release numbers, repeat without CPU profiling and synchronous stage logging. + +Successful data comparison included nodes, edges, files, unresolved references, import bindings, aliases, source chunks, fingerprints, LSH joins by semantic node identity, grounding baselines, schema versions and metadata. Exclusions were generated row IDs and operational write/index timestamps; snapshot source/config/compiler/grammar digests and Git coordinates remained compared. Both databases passed `quick_check` and foreign-key checks. Four ranked node/source FTS query result sets matched. This does not claim byte-identical database files, exhaustive FTS/API equivalence, or production failure-path coverage. + +Verification for this investigation: + +- Public API and emitted declarations: unchanged. +- Ordinary reads, authority and snapshot publication: production code unchanged; experiments wrote only temporary copies. +- Bounds: no production limits changed; sequential temporary runs used the diagnostic supervisor described above. +- Focused validation: full-data parity, SQLite scaling/failure checks and repeated-process observations completed. Typecheck/full tests were not run because no production TypeScript changed. +- Build/evaluator: no package or graph protocol change; no production rebuild or evaluator regeneration performed. +- Working tree: report and focused MEX context notes only; generated databases, profiles, local state and builds remain untracked/ignored outside the change. +- Protocol and recovery: current behavior preserved; proposed implementation still requires the targeted tests listed above. + +MEX context used: architecture, `safe-graph-snapshot-evolution`, `release-performance-gate`, and `fresh-graph-hub-integration`. Historical Timeline search returned no matching notes. The investigation does not re-baseline grounding or promote speculative optimization claims as implemented behavior. diff --git a/docs/design/graph-isolation-diagnostic.json b/docs/design/graph-isolation-diagnostic.json new file mode 100644 index 00000000..dd31166a --- /dev/null +++ b/docs/design/graph-isolation-diagnostic.json @@ -0,0 +1,773 @@ +{ + "kind": "local-causal-diagnostic-not-pinned-calibration", + "purpose": "Bounded same-code causal diagnostic of disposable graph candidate processes versus a reused in-process runtime. This local result is not pinned release calibration and must not supply release budget values.", + "generatedAt": "2026-09-08T23:17:48.191Z", + "sourceHead": "4d6683eec1a0bdcafe99d7b431d84cde7f02864d", + "worktreeCleanAfterMeasurement": true, + "rawReportSha256": "7926b080d863bc40ac00dddc1c733e14d65d117f143424d4b3ff8629dd9fcdfb", + "environment": { + "node": "v22.17.1", + "sqlite": "3.50.0", + "v8": "12.4.254.21-node.27", + "platform": "darwin", + "arch": "arm64" + }, + "design": { + "fixture": "release-small-four-TypeScript-files", + "warmupsPerMode": 2, + "measuredPerMode": 3, + "sameOptimizedEngineAndParentValidation": true, + "separatePersistentParentPerMode": true, + "forcedGc": false, + "resourceScope": "maintenance parent and observed descendants", + "sampler": { + "scope": "hub-and-descendants", + "rss": "sum-of-resident-sets-in-one-sample", + "cpu": "observed-process-lifetime-deltas", + "intervalMs": 100, + "limitation": "Shared pages may be counted twice; short-lived children and final CPU between samples may be missed." + }, + "note": "Direct rebuildGraph diagnostic, not a Hub HTTP benchmark. Both modes include the same parent maintenance and publication validation. Process mode launches a disposable current-source candidate each time; in-process mode reuses loaded runtime/JIT state.", + "modeOrder": [ + "process", + "in-process" + ], + "limitations": "One local macOS allocation and fixed mode order; three measured runs per mode establish a bounded causal diagnostic, not pinned release calibration or an exact import/JIT/watchdog cost decomposition.", + "harness": "Temporary source bundles built with esbuild (Node 22 target, ESM, external dependencies) against the checkout dependency installation. Schema and grammar assets copied alongside. Separate copies of the initialized frozen small release fixture; each mode uses one persistent parent for five rebuilds. No measured Node flags or forced GC.", + "bounds": { + "externalDeadlineMs": 120000, + "workerReplyDeadlineMs": 30000, + "aggregateSampledRssGuardBytes": 2147483648 + } + }, + "bundleHashes": { + "worker.mjs": "5179040af2e643c9a967b687e6151acb4c81f4de8475d2bad92bb9ab88168ee8", + "graph-candidate.js": "55e89707ff0baa2516b049e56090c4629cb6e69091caee715ae3413f2952a3e3" + }, + "measurement": { + "elapsedMs": "Time around rebuildGraph only; excludes the following logical graph digest, IPC delivery and sampler shutdown.", + "parentCpuMs": "process.cpuUsage delta around rebuildGraph in its persistent parent; excludes candidate child CPU.", + "marks": "Milliseconds from the rebuildGraph start to observed progress or internal hook callbacks; callbacks available in each mode differ.", + "returnMemory": "Parent process.memoryUsage after rebuildGraph and the logical graph digest; all fields are bytes. This is a surviving-process snapshot, not a forced-GC retained-heap measurement.", + "resources": "Aggregate sampled maintenance parent and observed descendants, from before the invocation through result delivery, including the digest and IPC interval. RSS fields are bytes, cpuMs is observed process-lifetime CPU deltas; maxProcesses is the largest sampled process count.", + "summary": "Per-mode elapsed medians and aggregate peak RSS use only the three measured iterations. Final parent RSS is from the last iteration. All ten original iterations remain below.", + "samplerScopeNote": "The reused release sampler metadata names its root hub-and-descendants; this diagnostic roots it at the direct maintenance worker, with no Hub HTTP server." + }, + "parity": { + "allTenSucceededAndFresh": true, + "commonResult": { + "state": "succeeded", + "filesIndexed": 4, + "nodesCreated": 32, + "edgesCreated": 38, + "status": "fresh", + "parseHealth": { + "total": 4, + "ok": 4, + "partial": 0, + "failed": 0, + "failedPaths": [], + "failedPathsTruncated": false + } + }, + "allCoreDigestsEqual": true, + "allCountsEqual": true, + "digestMethod": { + "algorithm": "SHA-256 of JSON.stringify({nodes,edges,files,fingerprints,refs}).", + "nodes": "All fields except updated_at; rows sorted by JSON.stringify(row).localeCompare.", + "edges": "All fields except autoincrement id; same row sorting.", + "files": "All fields except modified_at and indexed_at; same row sorting.", + "fingerprints": "node_id, hex(minhash) AS minhash, neighbors, token_count, ordered by node_id.", + "refs": "All unresolved_refs fields except autoincrement id; same row sorting.", + "scope": "Logical core graph parity only; does not compare every table, timestamps, metadata or physical SQLite bytes." + } + }, + "samples": [ + { + "mode": "process", + "iteration": 1, + "warmup": true, + "elapsedMs": 723.099625, + "parentCpuMs": 84.225, + "marks": [ + { + "phase": "discover", + "atMs": 15.368707999999998 + }, + { + "phase": "stage", + "atMs": 54.215457999999984 + }, + { + "phase": "candidate_spawn", + "atMs": 55.55016599999999 + }, + { + "phase": "parse", + "atMs": 374.1006249999999 + }, + { + "phase": "resolve", + "atMs": 619.751166 + }, + { + "phase": "candidate_built", + "atMs": 660.518375 + }, + { + "phase": "validate", + "atMs": 660.5649999999999 + }, + { + "phase": "candidate_validated", + "atMs": 682.991958 + }, + { + "phase": "publish", + "atMs": 691.125541 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 162807808, + "heapTotal": 54312960, + "heapUsed": 42425752, + "external": 15931914, + "arrayBuffers": 13751505 + }, + "resources": { + "rssBytes": 162824192, + "peakRssBytes": 395182080, + "cpuMs": 1040, + "maxProcesses": 2, + "sampleCount": 9 + } + }, + { + "mode": "process", + "iteration": 2, + "warmup": true, + "elapsedMs": 904.0846670000001, + "parentCpuMs": 54.588, + "marks": [ + { + "phase": "discover", + "atMs": 10.706292000000076 + }, + { + "phase": "stage", + "atMs": 35.59466700000007 + }, + { + "phase": "candidate_spawn", + "atMs": 36.57787500000006 + }, + { + "phase": "parse", + "atMs": 557.8742500000001 + }, + { + "phase": "resolve", + "atMs": 808.972334 + }, + { + "phase": "candidate_built", + "atMs": 847.72775 + }, + { + "phase": "validate", + "atMs": 847.7717500000001 + }, + { + "phase": "candidate_validated", + "atMs": 868.7050839999999 + }, + { + "phase": "publish", + "atMs": 876.963375 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 168689664, + "heapTotal": 54575104, + "heapUsed": 39737784, + "external": 2273319, + "arrayBuffers": 92870 + }, + "resources": { + "rssBytes": 168689664, + "peakRssBytes": 405307392, + "cpuMs": 1060, + "maxProcesses": 2, + "sampleCount": 10 + } + }, + { + "mode": "process", + "iteration": 3, + "warmup": false, + "elapsedMs": 876.0462910000001, + "parentCpuMs": 44.764, + "marks": [ + { + "phase": "discover", + "atMs": 11.62154099999998 + }, + { + "phase": "stage", + "atMs": 35.61345800000004 + }, + { + "phase": "candidate_spawn", + "atMs": 36.47279100000014 + }, + { + "phase": "parse", + "atMs": 527.4614999999999 + }, + { + "phase": "resolve", + "atMs": 778.0508330000002 + }, + { + "phase": "candidate_built", + "atMs": 818.0947500000002 + }, + { + "phase": "validate", + "atMs": 818.1847910000001 + }, + { + "phase": "candidate_validated", + "atMs": 838.8774579999999 + }, + { + "phase": "publish", + "atMs": 847.9189160000001 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 169541632, + "heapTotal": 54837248, + "heapUsed": 43514160, + "external": 15932052, + "arrayBuffers": 13751603 + }, + "resources": { + "rssBytes": 169656320, + "peakRssBytes": 396771328, + "cpuMs": 920, + "maxProcesses": 3, + "sampleCount": 10 + } + }, + { + "mode": "process", + "iteration": 4, + "warmup": false, + "elapsedMs": 879.5032920000003, + "parentCpuMs": 64.856, + "marks": [ + { + "phase": "discover", + "atMs": 14.678417000000081 + }, + { + "phase": "stage", + "atMs": 39.59062500000027 + }, + { + "phase": "candidate_spawn", + "atMs": 40.370792000000165 + }, + { + "phase": "parse", + "atMs": 534.1774170000003 + }, + { + "phase": "resolve", + "atMs": 786.0798750000004 + }, + { + "phase": "candidate_built", + "atMs": 822.985709 + }, + { + "phase": "validate", + "atMs": 823.0244170000001 + }, + { + "phase": "candidate_validated", + "atMs": 842.3342500000003 + }, + { + "phase": "publish", + "atMs": 851.2854590000002 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 171524096, + "heapTotal": 54837248, + "heapUsed": 37172464, + "external": 2249601, + "arrayBuffers": 69152 + }, + "resources": { + "rssBytes": 171524096, + "peakRssBytes": 417382400, + "cpuMs": 1080, + "maxProcesses": 2, + "sampleCount": 10 + } + }, + { + "mode": "process", + "iteration": 5, + "warmup": false, + "elapsedMs": 882.3670000000002, + "parentCpuMs": 69.309, + "marks": [ + { + "phase": "discover", + "atMs": 18.476292000000285 + }, + { + "phase": "stage", + "atMs": 40.62395900000001 + }, + { + "phase": "candidate_spawn", + "atMs": 41.56916700000011 + }, + { + "phase": "parse", + "atMs": 531.2083750000002 + }, + { + "phase": "resolve", + "atMs": 788.0578750000004 + }, + { + "phase": "candidate_built", + "atMs": 826.7629590000006 + }, + { + "phase": "validate", + "atMs": 826.803167 + }, + { + "phase": "candidate_validated", + "atMs": 846.7960840000005 + }, + { + "phase": "publish", + "atMs": 854.2553749999997 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 171917312, + "heapTotal": 55099392, + "heapUsed": 37237992, + "external": 2249594, + "arrayBuffers": 69145 + }, + "resources": { + "rssBytes": 171917312, + "peakRssBytes": 423313408, + "cpuMs": 1090, + "maxProcesses": 2, + "sampleCount": 10 + } + }, + { + "mode": "in-process", + "iteration": 1, + "warmup": true, + "elapsedMs": 409.47450000000003, + "parentCpuMs": 803.312, + "marks": [ + { + "phase": "discover", + "atMs": 15.52600000000001 + }, + { + "phase": "stage", + "atMs": 55.139667 + }, + { + "phase": "candidate_built", + "atMs": 351.91925000000003 + }, + { + "phase": "validate", + "atMs": 351.98066700000004 + }, + { + "phase": "candidate_validated", + "atMs": 373.81508400000007 + }, + { + "phase": "publish", + "atMs": 380.37912500000004 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 244318208, + "heapTotal": 124092416, + "heapUsed": 94566096, + "external": 14909094, + "arrayBuffers": 12728685 + }, + "resources": { + "rssBytes": 244285440, + "peakRssBytes": 244285440, + "cpuMs": 810, + "maxProcesses": 1, + "sampleCount": 5 + } + }, + { + "mode": "in-process", + "iteration": 2, + "warmup": true, + "elapsedMs": 260.835333, + "parentCpuMs": 451.76, + "marks": [ + { + "phase": "discover", + "atMs": 14.208292000000029 + }, + { + "phase": "stage", + "atMs": 39.153625000000034 + }, + { + "phase": "candidate_built", + "atMs": 200.53437500000007 + }, + { + "phase": "validate", + "atMs": 200.604875 + }, + { + "phase": "candidate_validated", + "atMs": 223.77587500000004 + }, + { + "phase": "publish", + "atMs": 232.78370800000005 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 286752768, + "heapTotal": 109363200, + "heapUsed": 46445176, + "external": 14875744, + "arrayBuffers": 12695295 + }, + "resources": { + "rssBytes": 286752768, + "peakRssBytes": 294404096, + "cpuMs": 450, + "maxProcesses": 2, + "sampleCount": 4 + } + }, + { + "mode": "in-process", + "iteration": 3, + "warmup": false, + "elapsedMs": 228.46966699999996, + "parentCpuMs": 306.749, + "marks": [ + { + "phase": "discover", + "atMs": 14.26237500000002 + }, + { + "phase": "stage", + "atMs": 38.24895900000001 + }, + { + "phase": "candidate_built", + "atMs": 173.80383399999994 + }, + { + "phase": "validate", + "atMs": 173.85683400000005 + }, + { + "phase": "candidate_validated", + "atMs": 193.550167 + }, + { + "phase": "publish", + "atMs": 201.24904200000003 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 294895616, + "heapTotal": 125927424, + "heapUsed": 95962496, + "external": 27523170, + "arrayBuffers": 25342721 + }, + "resources": { + "rssBytes": 294895616, + "peakRssBytes": 295157760, + "cpuMs": 310, + "maxProcesses": 2, + "sampleCount": 4 + } + }, + { + "mode": "in-process", + "iteration": 4, + "warmup": false, + "elapsedMs": 220.15945899999997, + "parentCpuMs": 243.71, + "marks": [ + { + "phase": "discover", + "atMs": 13.114542000000029 + }, + { + "phase": "stage", + "atMs": 37.133959000000004 + }, + { + "phase": "candidate_built", + "atMs": 166.501667 + }, + { + "phase": "validate", + "atMs": 166.54874999999993 + }, + { + "phase": "candidate_validated", + "atMs": 185.73087499999997 + }, + { + "phase": "publish", + "atMs": 193.30279199999995 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 302596096, + "heapTotal": 172113920, + "heapUsed": 142000784, + "external": 28605640, + "arrayBuffers": 26425191 + }, + "resources": { + "rssBytes": 302596096, + "peakRssBytes": 302596096, + "cpuMs": 250, + "maxProcesses": 1, + "sampleCount": 4 + } + }, + { + "mode": "in-process", + "iteration": 5, + "warmup": false, + "elapsedMs": 219.6812910000001, + "parentCpuMs": 244.137, + "marks": [ + { + "phase": "discover", + "atMs": 13.49958300000003 + }, + { + "phase": "stage", + "atMs": 36.46279100000015 + }, + { + "phase": "candidate_built", + "atMs": 163.14637500000003 + }, + { + "phase": "validate", + "atMs": 163.19095800000014 + }, + { + "phase": "candidate_validated", + "atMs": 184.39962500000001 + }, + { + "phase": "publish", + "atMs": 192.67262500000015 + } + ], + "graph": { + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "sha256": "3aab6bb4fc5db62e8dd60c90428f49dd08629937307b01897cfe4d8b08a7ba04" + }, + "returnMemory": { + "rss": 336035840, + "heapTotal": 130334720, + "heapUsed": 50367624, + "external": 14899399, + "arrayBuffers": 12718950 + }, + "resources": { + "rssBytes": 336035840, + "peakRssBytes": 336216064, + "cpuMs": 240, + "maxProcesses": 2, + "sampleCount": 4 + } + } + ], + "summary": { + "process": { + "samples": [ + 876.0462910000001, + 879.5032920000003, + 882.3670000000002 + ], + "medianMs": 879.5032920000003, + "maxRssBytes": 423313408, + "observedCpuMs": [ + 920, + 1080, + 1090 + ], + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "finalParentRssBytes": 171917312, + "sampleOrder": "Elapsed samples are sorted ascending; observedCpuMs follows measured iteration order. Per-iteration pairs are retained in samples." + }, + "in-process": { + "samples": [ + 219.6812910000001, + 220.15945899999997, + 228.46966699999996 + ], + "medianMs": 220.15945899999997, + "maxRssBytes": 336216064, + "observedCpuMs": [ + 310, + 250, + 240 + ], + "counts": { + "nodes": 32, + "edges": 38, + "files": 4, + "fingerprints": 16, + "refs": 8 + }, + "finalParentRssBytes": 336035840, + "sampleOrder": "Elapsed samples are sorted ascending; observedCpuMs follows measured iteration order. Per-iteration pairs are retained in samples." + } + }, + "interpretation": { + "medianDifferenceMs": 659.3438330000004, + "medianRatio": 3.994846716987983, + "finding": "On this small fixture, disposable candidate processes take longer and have a higher sampled aggregate peak RSS, while leaving a lower-RSS surviving parent. Equal logical core outputs and matching parent validation support attribution to candidate lifecycle/runtime reuse rather than different graph work.", + "limits": "One local macOS allocation, fixed mode order and three measured iterations per mode. No precise import/JIT/watchdog cost decomposition, general peak-memory improvement, leak conclusion, large-project forecast or pinned budget calibration is established." + }, + "outcome": "passed", + "totalDiagnosticElapsedMs": 8658.5395 +} diff --git a/docs/design/graph-maintenance-timing-calibration.json b/docs/design/graph-maintenance-timing-calibration.json new file mode 100644 index 00000000..020591d0 --- /dev/null +++ b/docs/design/graph-maintenance-timing-calibration.json @@ -0,0 +1,871 @@ +{ + "schemaVersion": 1, + "kind": "graph-maintenance-timing-calibration", + "source": { + "runUrl": "https://github.com/mex-memory/mex/actions/runs/34288560611", + "pullRequest": 180, + "pullRequestHead": "4d6683eec1a0bdcafe99d7b431d84cde7f02864d", + "repositoryHead": "6d92bb04d757c8a00693ef679d1f4281669a9b57", + "primaryAttempt": "first", + "primarySelection": "The first corrected pinned attempt is the deterministic calibration source; the second independently confirms the old-budget tradeoff and is not pooled or selected per metric.", + "finalReportSha256": "66bb54c302ea6cb41084c668007711eacf65abd83931f2fe81d325277ef746a5" + }, + "environment": { + "os": "ubuntu-24.04", + "platform": "linux", + "architecture": "x64", + "node": "v22.22.0", + "pinnedBudgetEnvironment": true + }, + "configuration": { + "fixtureProfiles": { + "small": { + "sourceFiles": 4, + "wikiEntities": 4, + "workstreams": 1, + "inboxDrafts": 1, + "inboxProposals": 1, + "members": 2, + "relayDrafts": 1, + "relays": 1, + "activityEvents": 4 + }, + "medium": { + "sourceFiles": 16, + "wikiEntities": 16, + "workstreams": 1, + "inboxDrafts": 1, + "inboxProposals": 1, + "members": 2, + "relayDrafts": 1, + "relays": 1, + "activityEvents": 16 + }, + "large": { + "sourceFiles": 48, + "wikiEntities": 48, + "workstreams": 1, + "inboxDrafts": 1, + "inboxProposals": 1, + "members": 2, + "relayDrafts": 1, + "relays": 1, + "activityEvents": 48 + } + }, + "processMeasurement": { + "scope": "hub-and-descendants", + "rss": "sum-of-resident-sets-in-one-sample", + "cpu": "observed-process-lifetime-deltas", + "intervalMs": 10, + "limitation": "Shared pages may be counted twice; short-lived children and final CPU between samples may be missed." + }, + "maintenanceObservation": "job-event-stream", + "samples": { + "timing": 10, + "idleMemory": 5 + }, + "runtimeBudgetsEnforced": true, + "assetBudgetsEnforced": true, + "provisionalBudgets": false + }, + "formula": "ceil(measured p95 * 1.15)", + "sampleCount": 10, + "previousCalibrationStatus": "pinned-checkpoints-A-G-and-Settings-34286120355", + "unownedBudgetSha256": "d4209e5549ed49c37dcd4e4814eab0ceb7640124d75bdf433347ea9de1514c80", + "calibratedLeaves": [ + { + "metric": "runtime.maintenanceMs.small.graph_refresh", + "previousBudgetMs": 984, + "primaryP95Ms": 1420.61, + "budgetMs": 1634 + }, + { + "metric": "runtime.maintenanceMs.small.graph_rebuild", + "previousBudgetMs": 496, + "primaryP95Ms": 1468.48, + "budgetMs": 1689 + }, + { + "metric": "runtime.maintenanceMs.medium.graph_refresh", + "previousBudgetMs": 1237, + "primaryP95Ms": 1600.897, + "budgetMs": 1842 + }, + { + "metric": "runtime.maintenanceMs.medium.graph_rebuild", + "previousBudgetMs": 743, + "primaryP95Ms": 1581.908, + "budgetMs": 1820 + }, + { + "metric": "runtime.maintenanceMs.large.graph_rebuild", + "previousBudgetMs": 1229, + "primaryP95Ms": 1980.154, + "budgetMs": 2278 + } + ], + "unchangedLargeGraphRefreshMs": 1812, + "attempts": [ + { + "role": "primary", + "manifest": { + "schemaVersion": 1, + "kind": "mex-release-benchmark-attempt", + "repositoryHead": "6d92bb04d757c8a00693ef679d1f4281669a9b57", + "github": { + "runId": "34288560611", + "runAttempt": "1", + "sha": "6d92bb04d757c8a00693ef679d1f4281669a9b57" + }, + "runnerAllocation": { + "job": "release_performance_attempt_1", + "runnerName": "GitHub Actions 1000000935", + "runnerOs": "Linux", + "runnerArch": "X64" + }, + "rawReportSha256": "fec98eae2728a017fcaf3480d275d47180d88740b248468d69a6fa858768c9b3", + "retryRequired": true, + "disposition": "confirmation_required" + }, + "graph": { + "small": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1341.863, + 1345.533, + 1368.565, + 1373.907, + 1378.077, + 1383.913, + 1387.332, + 1390.599, + 1416.879, + 1420.61 + ], + "min": 1341.863, + "median": 1380.995, + "p95": 1420.61, + "max": 1420.61 + }, + "peakRssBytes": { + "samples": [ + 517021696, + 521039872, + 522711040, + 523661312, + 526897152 + ], + "min": 517021696, + "median": 522711040, + "p95": 526897152, + "max": 526897152 + }, + "cpuMs": { + "samples": [ + 2390, + 2410, + 2430, + 2450, + 2460, + 2460, + 2490, + 2500, + 2520, + 2520 + ], + "min": 2390, + "median": 2460, + "p95": 2520, + "max": 2520 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1391.8, + 1391.816, + 1398.561, + 1413.862, + 1417.463, + 1423.925, + 1429.694, + 1430.895, + 1431.209, + 1468.48 + ], + "min": 1391.8, + "median": 1420.694, + "p95": 1468.48, + "max": 1468.48 + }, + "peakRssBytes": { + "samples": [ + 422674432, + 423641088, + 429113344, + 430256128, + 430649344 + ], + "min": 422674432, + "median": 429113344, + "p95": 430649344, + "max": 430649344 + }, + "cpuMs": { + "samples": [ + 2520, + 2530, + 2570, + 2590, + 2610, + 2630, + 2640, + 2640, + 2660, + 2800 + ], + "min": 2520, + "median": 2620, + "p95": 2800, + "max": 2800 + } + } + }, + "medium": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1520.31, + 1531.869, + 1532.251, + 1534.805, + 1542.041, + 1551.169, + 1560.108, + 1577.819, + 1581.672, + 1600.897 + ], + "min": 1520.31, + "median": 1546.605, + "p95": 1600.897, + "max": 1600.897 + }, + "peakRssBytes": { + "samples": [ + 598917120, + 600551424, + 600866816, + 601272320, + 635428864 + ], + "min": 598917120, + "median": 600866816, + "p95": 635428864, + "max": 635428864 + }, + "cpuMs": { + "samples": [ + 2740, + 2760, + 2780, + 2800, + 2800, + 2800, + 2850, + 2850, + 2900, + 2960 + ], + "min": 2740, + "median": 2800, + "p95": 2960, + "max": 2960 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1546.797, + 1555.926, + 1556.308, + 1557.454, + 1566.463, + 1567.502, + 1576.954, + 1578.601, + 1581.423, + 1581.908 + ], + "min": 1546.797, + "median": 1566.983, + "p95": 1581.908, + "max": 1581.908 + }, + "peakRssBytes": { + "samples": [ + 602345472, + 603676672, + 638533632, + 639234048, + 642658304 + ], + "min": 602345472, + "median": 638533632, + "p95": 642658304, + "max": 642658304 + }, + "cpuMs": { + "samples": [ + 2760, + 2770, + 2770, + 2770, + 2780, + 2800, + 2810, + 2830, + 2840, + 2840 + ], + "min": 2760, + "median": 2790, + "p95": 2840, + "max": 2840 + } + } + }, + "large": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1892.69, + 1898.16, + 1904.118, + 1917.955, + 1923.385, + 1923.55, + 1939.172, + 1949.167, + 1953.842, + 1955.065 + ], + "min": 1892.69, + "median": 1923.468, + "p95": 1955.065, + "max": 1955.065 + }, + "peakRssBytes": { + "samples": [ + 610271232, + 614817792, + 614899712, + 615059456, + 615129088 + ], + "min": 610271232, + "median": 614899712, + "p95": 615129088, + "max": 615129088 + }, + "cpuMs": { + "samples": [ + 3320, + 3330, + 3340, + 3380, + 3380, + 3380, + 3380, + 3390, + 3410, + 3440 + ], + "min": 3320, + "median": 3380, + "p95": 3440, + "max": 3440 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1910.85, + 1918.468, + 1921.088, + 1929.351, + 1930.704, + 1947.294, + 1954.774, + 1964.977, + 1977.912, + 1980.154 + ], + "min": 1910.85, + "median": 1938.999, + "p95": 1980.154, + "max": 1980.154 + }, + "peakRssBytes": { + "samples": [ + 615219200, + 615550976, + 615976960, + 617656320, + 618491904 + ], + "min": 615219200, + "median": 615976960, + "p95": 618491904, + "max": 618491904 + }, + "cpuMs": { + "samples": [ + 3330, + 3330, + 3360, + 3370, + 3370, + 3380, + 3390, + 3430, + 3440, + 3450 + ], + "min": 3330, + "median": 3375, + "p95": 3450, + "max": 3450 + } + } + } + } + }, + { + "role": "confirmation", + "manifest": { + "schemaVersion": 1, + "kind": "mex-release-benchmark-attempt", + "repositoryHead": "6d92bb04d757c8a00693ef679d1f4281669a9b57", + "github": { + "runId": "34288560611", + "runAttempt": "1", + "sha": "6d92bb04d757c8a00693ef679d1f4281669a9b57" + }, + "runnerAllocation": { + "job": "release_performance_attempt_2", + "runnerName": "GitHub Actions 1000000940", + "runnerOs": "Linux", + "runnerArch": "X64" + }, + "rawReportSha256": "732d2b897b4592ba7e89d9fedb508c0c86027ae7cb239580ed51aa5a9de9efb9", + "retryRequired": true, + "disposition": "confirmation_required" + }, + "graph": { + "small": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1123.844, + 1181.976, + 1251.239, + 1263.552, + 1356.591, + 1405.457, + 1441.052, + 1594.281, + 1613.055, + 1853.568 + ], + "min": 1123.844, + "median": 1381.024, + "p95": 1853.568, + "max": 1853.568 + }, + "peakRssBytes": { + "samples": [ + 513835008, + 516476928, + 524251136, + 527007744, + 559038464 + ], + "min": 513835008, + "median": 524251136, + "p95": 559038464, + "max": 559038464 + }, + "cpuMs": { + "samples": [ + 2050, + 2070, + 2070, + 2100, + 2110, + 2120, + 2130, + 2130, + 2150, + 2160 + ], + "min": 2050, + "median": 2115, + "p95": 2160, + "max": 2160 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1304.684, + 1315.851, + 1348.025, + 1389.671, + 1391.572, + 1409.607, + 1414.26, + 1460.416, + 1475.798, + 1608.501 + ], + "min": 1304.684, + "median": 1400.59, + "p95": 1608.501, + "max": 1608.501 + }, + "peakRssBytes": { + "samples": [ + 421404672, + 429764608, + 525230080, + 532393984, + 532467712 + ], + "min": 421404672, + "median": 525230080, + "p95": 532467712, + "max": 532467712 + }, + "cpuMs": { + "samples": [ + 2080, + 2100, + 2180, + 2280, + 2280, + 2290, + 2300, + 2310, + 2330, + 2360 + ], + "min": 2080, + "median": 2285, + "p95": 2360, + "max": 2360 + } + } + }, + "medium": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1271.705, + 1285.42, + 1314.929, + 1329.755, + 1447.731, + 1553.504, + 1571.778, + 1606.949, + 1640.042, + 1714.77 + ], + "min": 1271.705, + "median": 1500.617, + "p95": 1714.77, + "max": 1714.77 + }, + "peakRssBytes": { + "samples": [ + 605356032, + 609439744, + 609964032, + 612069376, + 615657472 + ], + "min": 605356032, + "median": 609964032, + "p95": 615657472, + "max": 615657472 + }, + "cpuMs": { + "samples": [ + 2370, + 2380, + 2390, + 2400, + 2400, + 2420, + 2430, + 2450, + 2460, + 2470 + ], + "min": 2370, + "median": 2410, + "p95": 2470, + "max": 2470 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1323.223, + 1323.635, + 1327.093, + 1363.659, + 1419.472, + 1424.562, + 1438.274, + 1450.918, + 1506.022, + 1550.249 + ], + "min": 1323.223, + "median": 1422.017, + "p95": 1550.249, + "max": 1550.249 + }, + "peakRssBytes": { + "samples": [ + 610123776, + 610553856, + 612372480, + 612442112, + 612491264 + ], + "min": 610123776, + "median": 612372480, + "p95": 612491264, + "max": 612491264 + }, + "cpuMs": { + "samples": [ + 2420, + 2450, + 2450, + 2470, + 2470, + 2480, + 2480, + 2490, + 2500, + 2510 + ], + "min": 2420, + "median": 2475, + "p95": 2510, + "max": 2510 + } + } + }, + "large": { + "graph_refresh": { + "elapsedMs": { + "samples": [ + 1625.325, + 1629.425, + 1647.157, + 1671.584, + 1711.165, + 1819.815, + 1859.421, + 1904.886, + 1908.309, + 1925.563 + ], + "min": 1625.325, + "median": 1765.49, + "p95": 1925.563, + "max": 1925.563 + }, + "peakRssBytes": { + "samples": [ + 620830720, + 627740672, + 629227520, + 629358592, + 632696832 + ], + "min": 620830720, + "median": 629227520, + "p95": 632696832, + "max": 632696832 + }, + "cpuMs": { + "samples": [ + 2850, + 2870, + 2870, + 2880, + 2890, + 2920, + 2920, + 2920, + 2990, + 3020 + ], + "min": 2850, + "median": 2905, + "p95": 3020, + "max": 3020 + } + }, + "graph_rebuild": { + "elapsedMs": { + "samples": [ + 1585.808, + 1599.499, + 1600.839, + 1611.116, + 1612.166, + 1617.531, + 1929.309, + 1944.566, + 2009.876, + 2074.786 + ], + "min": 1585.808, + "median": 1614.849, + "p95": 2074.786, + "max": 2074.786 + }, + "peakRssBytes": { + "samples": [ + 626761728, + 632528896, + 635752448, + 642117632, + 643031040 + ], + "min": 626761728, + "median": 635752448, + "p95": 643031040, + "max": 643031040 + }, + "cpuMs": { + "samples": [ + 2790, + 2810, + 2820, + 2870, + 2890, + 2890, + 2900, + 2920, + 2930, + 2990 + ], + "min": 2790, + "median": 2890, + "p95": 2990, + "max": 2990 + } + } + } + } + } + ], + "confirmedMaterialAssessments": [ + { + "metric": "runtime.maintenanceMs.small.graph_refresh", + "category": "maintenance_ms", + "classification": "material", + "reason": "repeated_material_threshold", + "budget": 984, + "relativeExcessRatio": 0.15, + "minimumExcess": 50, + "materialThreshold": 1131.6, + "firstMeasured": 1420.61, + "secondMeasured": 1853.568, + "requiredSupportingSamples": 2, + "firstSampleCount": 10, + "firstSupportingSamples": 10, + "secondSampleCount": 10, + "secondSupportingSamples": 9 + }, + { + "metric": "runtime.maintenanceMs.small.graph_rebuild", + "category": "maintenance_ms", + "classification": "material", + "reason": "repeated_material_threshold", + "budget": 496, + "relativeExcessRatio": 0.15, + "minimumExcess": 50, + "materialThreshold": 570.4, + "firstMeasured": 1468.48, + "secondMeasured": 1608.501, + "requiredSupportingSamples": 2, + "firstSampleCount": 10, + "firstSupportingSamples": 10, + "secondSampleCount": 10, + "secondSupportingSamples": 10 + }, + { + "metric": "runtime.maintenanceMs.medium.graph_refresh", + "category": "maintenance_ms", + "classification": "material", + "reason": "repeated_material_threshold", + "budget": 1237, + "relativeExcessRatio": 0.15, + "minimumExcess": 50, + "materialThreshold": 1422.55, + "firstMeasured": 1600.897, + "secondMeasured": 1714.77, + "requiredSupportingSamples": 2, + "firstSampleCount": 10, + "firstSupportingSamples": 10, + "secondSampleCount": 10, + "secondSupportingSamples": 6 + }, + { + "metric": "runtime.maintenanceMs.medium.graph_rebuild", + "category": "maintenance_ms", + "classification": "material", + "reason": "repeated_material_threshold", + "budget": 743, + "relativeExcessRatio": 0.15, + "minimumExcess": 50, + "materialThreshold": 854.45, + "firstMeasured": 1581.908, + "secondMeasured": 1550.249, + "requiredSupportingSamples": 2, + "firstSampleCount": 10, + "firstSupportingSamples": 10, + "secondSampleCount": 10, + "secondSupportingSamples": 10 + }, + { + "metric": "runtime.maintenanceMs.large.graph_rebuild", + "category": "maintenance_ms", + "classification": "material", + "reason": "repeated_material_threshold", + "budget": 1229, + "relativeExcessRatio": 0.15, + "minimumExcess": 50, + "materialThreshold": 1413.35, + "firstMeasured": 1980.154, + "secondMeasured": 2074.786, + "requiredSupportingSamples": 2, + "firstSampleCount": 10, + "firstSupportingSamples": 10, + "secondSampleCount": 10, + "secondSupportingSamples": 10 + } + ], + "note": "The user accepted higher measured small-job wall-clock cost for responsive, cancellable Hub construction. This calibrates only these five Graph maintenance timing leaves with the existing formula; it is a product tradeoff, not a speedup claim. Large Graph refresh, all memory/asset/read/other limits, fixtures, materiality policy and confirmation rules remain unchanged. A clean enforcing run on the calibrated head is still required." +} diff --git a/docs/design/inbox-spec-authoring-contract.md b/docs/design/inbox-spec-authoring-contract.md index 17270038..c06224f1 100644 --- a/docs/design/inbox-spec-authoring-contract.md +++ b/docs/design/inbox-spec-authoring-contract.md @@ -1,5 +1,44 @@ # Inbox-Governed Spec Authoring Contract +> **0.8.1 extension:** Inbox also accepts explicit contributions to existing +> project knowledge. The additive contract below supersedes the Spec-only +> product scope; the original Spec format and its safety rules remain supported. + +## 0.8.1 knowledge contributions + +Inbox is the deliberate proposal/review path into canonical Wiki knowledge. +It adds no knowledge category and does not require routine GROW or ordinary +non-Spec Wiki upkeep to go through review. + +- `knowledge.create` accepts `entityKind` (architecture, component, convention, + decision, pattern, guide), title, body, optional summary/topics, and status + (`in_flight` or `promoted`). MEX mints the ID and path: patterns use + `patterns/.md`; other kinds use `context/-.md`. +- `knowledge.update` accepts one exact `target` (id, kind, optional title) and a + nonempty `patch` of title, summary, or body. A target can be an existing + document or section. The existing dependency revision rules apply. +- Knowledge creates have no relation editor. The closed legacy Spec relation + rules below still apply to existing Spec workflows. +- New knowledge requests use `mex.team.inbox.knowledge-change.v1`; existing + `mex.team.inbox.spec-change.v1` bytes are not migrated. The compatibility-named + private types, receipt domain, and `spec-entity` minted-ID receipt purpose are + retained for both create families. These are implementation tokens, not UI + categories. +- Approval carries a proposal source link and captured evidence into knowledge; + corrections append sources while retaining prior attribution and grounding. + Publication-time author and reviewer remain distinct in proposal history. +- The Inbox skill searches existing Wiki records before creating a duplicate. + `mex inbox target --json` provides a bounded immutable projection + of target text and exact `version.contentHash`/`semanticRevision` values. +- Local drafts are checkout-only. Publishing creates proposal Markdown in the + working tree. Approval writes canonical knowledge and review/audit artifacts. + Commit/push/pull remain the Git sharing steps. + +Existing bounds, exact preview/apply, containment, stale/repair, recovery, and +legacy Spec guards below continue to apply. The default Hub composer now uses +knowledge contributions, while older Spec drafts and proposals retain their +original review path. + Status: Checkpoints E1-E4 implemented; pinned calibration applied and enforcing CI required This brief freezes the minimum product boundary for checkout-local Inbox drafts, diff --git a/docs/design/relay-handoff-contract.md b/docs/design/relay-handoff-contract.md index f6b9c58a..f939be3d 100644 --- a/docs/design/relay-handoff-contract.md +++ b/docs/design/relay-handoff-contract.md @@ -1,6 +1,6 @@ # Relay Handoff Contract -Status: standalone schema-v3 Relay contract implemented; pinned release calibration retained and final enforcement gated +Status: 0.8.1 team-audience and Member recovery extension in progress; pinned release enforcement remains gated This brief freezes the repository-native Relay product: a checkout-local draft becomes one canonical handoff, one eligible recipient takes it, and the original @@ -8,9 +8,10 @@ sender or claimant closes it. A Relay is standalone team memory. It can carry structured progress and related context, but it does not belong to a Workstream, deliver messages, start agents, notify external systems, or execute work. -Repositories that will publish schema-v3 Relays must first update MEX for every -teammate. Older binaries cannot parse the strict v3 artifact. Existing schema-v1 -and schema-v2 Relays remain compatible and are never rewritten or backfilled. +Repositories publishing open-to-team schema-v4 Relays must use the 0.8.1-capable +CLI across the team. Older binaries cannot parse this new strict format. +Existing schema-v1, v2, and v3 Relays retain their original named recipients +and are never rewritten or backfilled. ## Lifecycle and authority @@ -24,7 +25,8 @@ Draft reads and save/update/delete are checkout-local. They require no current Member, Git identity, Wiki index, Workstream service, GitHub access, or network request. A new draft requires only: -- one to 32 unique canonical Member recipients; and +- an intended audience and an optional selection of up to 32 unique Member + recipients (an empty array is valid locally); and - a non-empty bounded, single-line summary. The optional `completed`, `inProgress`, `decisions`, `blockers`, @@ -45,13 +47,18 @@ at 64 entries. Publication resolves the current actor to one active canonical Member, through either the explicit checkout-local selection or one unique active Git alias. It -also proves that every recipient is active. Its exact expectation set is the +also proves that every named recipient is active. An explicit `team` audience +requires an empty recipient array and only the exact local draft revision. +A `members` audience requires one to 32 named recipients at publication; an +omitted audience retains this legacy meaning. Its exact expectation set is the local draft revision plus every unique recipient Member revision: no Workstream, duplicate, omission, semantic revision, or unrelated target is accepted. The same facts are checked under the repository workflow lease immediately before publication and when an unpublished intent is recovered. -Any listed active recipient may Take a published Relay. Exact Relay-revision +Any listed active recipient may Take a named Relay. An open-to-team Relay allows +any current active project Member, including one who joined after publication. +Exact Relay-revision conflict semantics make the first successful acknowledgement the sole claimant. Two unsynchronized clones can still attempt claims and later encounter a Git conflict; Relay does not provide a global network lock. Close requires @@ -60,8 +67,10 @@ close, and both recorded principals must remain active. Closing only removes the handoff from open attention and does not complete or modify a Workstream or task. Authorization compares stable `memberId` values, never display names or whole actor objects. Legacy Git, unknown, or inactive principals remain -readable but cannot act; reactivation is the only recovery for a stranded -Relay. +readable but cannot act. Explicit `member.reactivate` restores an inactive +Member with the same ID through a revision-bound preview/apply and one +`member.reactivated` Activity. Alias collisions and stale state fail closed; +reactivation never rewrites recorded actors or changes local Member selection. ## Canonical artifacts and Activity @@ -72,10 +81,13 @@ Relay artifacts are a strict schema-discriminated union: - schema v2 requires `workstream` and `published_at`, with no `published_repo_state`; - schema v3 rejects `workstream` and requires both `published_at` and - `published_repo_state`. + `published_repo_state`; +- schema v4 has the same standalone publication context, requires + `audience: team`, and requires an empty recipient array. -New publications always write schema v3. Their public Hub and CLI projections -expose `schemaVersion: 1 | 2 | 3`, `workstream: EntityRef | null`, and +Named publications still write schema v3; open-to-team publications write v4. +Their public Hub and CLI projections expose `schemaVersion: 1 | 2 | 3 | 4`, +optional `audience`, `workstream: EntityRef | null`, and `publishedRepoState: RepoState | null`. Legacy v1/v2 values retain their recorded Workstream and return no publication repository state. V3 returns a null Workstream and its immutable publication state. Acknowledge and Close @@ -101,7 +113,7 @@ not a reproducible source snapshot. Preview/apply revalidation detects branch, HEAD, or clean/dirty changes, but cannot detect dirty tree A changing into dirty tree B. -Publication writes the v3 Relay and exactly one `relay.published` Activity +Publication writes the v3 or v4 Relay and exactly one `relay.published` Activity before deleting the exact local draft. That Activity has the Relay as subject, omits Workstream, and records the same timestamp and repository state as the Relay publication. Take and Close each write exactly one Activity with their @@ -154,6 +166,22 @@ request and apply only the complete successful wrapper through reconstructed receipts, and mismatched action commands are rejected before a repository service is opened. +New local drafts also support `mex relay draft save --from +[--operation-id ] --json`. The bounded file contains only sparse draft +content. Omitted audience and recipient fields default to open-to-team; an +explicit named audience remains named even when its local recipient selection +is empty. The shortcut performs the same signed preview/apply internally and +requires a preview that only creates one local draft. It cannot publish, update, +or delete. Before applying, it saves the exact preview in an owner-only file +under `.mex/local/relay-previews/`; new saves stop at 64 occupied entries, +each limited to 64 KiB. Recovery permits one additional private staging file +left between atomic publication and cleanup. Staging files remain inert and +count against new-save capacity. Successful saves remove their exact pending file. +Interrupted saves retain it and can resume using the same operation ID and +unchanged content, or the returned exact `--apply` command. Conflicting requests +never replace an existing receipt. This content remains checkout-local and is +excluded from Git; it is not added to the metadata-only workflow journal. + Because Relay was not merged to `origin/main`, included in a release tag, or externally released before v3 implementation, the pre-release request/catalog v1 identifiers are updated in place. The static resolver still publishes diff --git a/docs/design/release-performance-baseline.md b/docs/design/release-performance-baseline.md index 43637730..9a76815f 100644 --- a/docs/design/release-performance-baseline.md +++ b/docs/design/release-performance-baseline.md @@ -110,6 +110,80 @@ changes only confirmation allocation and provenance: `budgets.json`, sample counts, material thresholds, category floors, and calibration formulas remain byte-for-byte unchanged. +The 0.8.1 Settings route is included in the route manifest, isolated asset +closures, and per-profile browser heap measurement. Its readiness check waits +for the loaded logging preference form and current selection, so a loading or +unavailable page cannot satisfy measurement. Settings must remain outside the +initial shell and Home closures. Only its additive asset limits may be copied +from the final deterministic build using `ceil(bytes * 1.05)`. + +At the Phase 4 checkpoint, the three Settings heap budget leaves remained +absent, so enforcement emitted `budget_missing` and blocked release. Initial +PR #180 CI run +[`34286120355`](https://github.com/mex-memory/mex/actions/runs/34286120355) +then supplied the retained, schema-valid measurement report on Ubuntu 24.04, +Linux x64, Node 22.22.0. Artifact `10079816681` measured PR head +`6e12e6dd34a3bb11735ce3580aece8d37e2e8043` through synthetic merge commit +`8c046500e2969463014e12dc5844abf76ae403ce`; the raw report SHA-256 was verified as +`98007786b450eb7a6142d0ce43cf2bb6fc85c287d6e0a8837850318d5924fd04`. + +Only the three missing Settings heap leaves and calibration-status metadata +are now added using the frozen `ceil(p95 * 1.15)` formula: + +| Profile | Measured Settings heap p95 | New limit | +|---|---:|---:| +| Small | 5,480,428 bytes | 6,302,493 bytes | +| Medium | 5,482,104 bytes | 6,304,420 bytes | +| Large | 5,486,028 bytes | 6,308,933 bytes | + +The [retained calibration evidence](settings-heap-calibration.json) records all +five raw samples per profile, exact report identity, formula, and a hash of +every unowned budget. Existing Graph and other runtime/asset limits, sample +counts, material thresholds, and confirmation rules remain unchanged. Settings +stays an additive optional schema field so historical reports remain valid. + +That first CI run still failed enforcement: the missing leaves produced an +immediate hard failure and suppressed runtime confirmation. Its first-pass +Graph maintenance crossings are unconfirmed, not a runtime pass or an +established regression. A clean enforcing run on the corrected final head must +apply the ordinary fresh-runner confirmation rule; calibration alone does not +satisfy the release gate. + +### Accepted graph isolation timing tradeoff + +Corrected PR #180 run +[`34288560611`](https://github.com/mex-memory/mex/actions/runs/34288560611) +passed browser, Node 22/24, and Windows/macOS portability checks. Its two +independently allocated pinned runners confirmed exactly five material Graph +maintenance timing failures on PR head +`4d6683eec1a0bdcafe99d7b431d84cde7f02864d`, synthetic merge +`6d92bb04d757c8a00693ef679d1f4281669a9b57`. Repeated memory crossings remained +advisory under the existing materiality/sample-support rules; no memory or +other metric produced a final material failure. + +The product decision explicitly accepts disposable-worker startup latency for +Hub responsiveness and compiler-memory release after each job. This is a real +small-job regression. Only the five confirmed timing leaves are recalibrated +from the first healthy corrected report using the existing `ceil(p95 * 1.15)` +formula; the second allocation supplies independent confirmation. + +| Graph operation | Prior limit (ms) | First p95 (ms) | Confirmation p95 (ms) | New limit (ms) | +|---|---:|---:|---:|---:| +| Small refresh | 984 | 1420.610 | 1853.568 | 1634 | +| Small rebuild | 496 | 1468.480 | 1608.501 | 1689 | +| Medium refresh | 1237 | 1600.897 | 1714.770 | 1842 | +| Medium rebuild | 743 | 1581.908 | 1550.249 | 1820 | +| Large rebuild | 1229 | 1980.154 | 2074.786 | 2278 | + +The [calibration record](graph-maintenance-timing-calibration.json) retains +runner identities, both raw-report hashes and samples, prior limits, and a hash +guard for every unowned budget. Large refresh, all memory/asset/read/Wiki +limits, fixtures, formulas, sample counts, and confirmation rules remain +unchanged. The [local diagnostic](graph-isolation-diagnostic.json) attributes +the fixed startup cost using identical optimized code and parent validation; +its Mac timings are not calibration inputs. A clean enforcing CI run on the +new calibrated head remains required before release. + ## Runner contract `npm run benchmark:release` builds the package and writes the bounded JSON @@ -150,7 +224,7 @@ Each profile records: Every profile additionally records five Chromium heap samples after every registered Hub route: Home, Search, Knowledge browse/detail, Code search/symbol, Workstreams, Specs browse/detail, Inbox, Relay, the honest unavailable Playbooks -route, Members, Activity, Jobs, Health, and the wildcard not-found route. +route, Members, Activity, Jobs, Health, Settings, and the wildcard not-found route. Every browser context begins empty. Its request audit fails if a route contacts any origin other than the exact loopback Hub origin. @@ -159,7 +233,7 @@ initial static import closure and the incremental JavaScript, CSS, and font bytes for every registered route. Fonts referenced from global CSS are counted as initial assets even when Vite does not attach them to a manifest entry. The initial shell and Home must not statically close over Code, Knowledge, -Workstreams, Specs, Inbox, Relay, Members, Activity, or setup code, and the +Workstreams, Specs, Inbox, Relay, Members, Activity, Settings, or setup code, and the largest JavaScript chunk is checked explicitly. The Activity route is a read-only workbench and has no nested manual-recorder chunk. Its source controls, default feed, and accessible shadcn Collapsible controls remain in diff --git a/docs/design/settings-heap-calibration.json b/docs/design/settings-heap-calibration.json new file mode 100644 index 00000000..0809b523 --- /dev/null +++ b/docs/design/settings-heap-calibration.json @@ -0,0 +1,83 @@ +{ + "schemaVersion": 1, + "kind": "settings-browser-heap-calibration", + "source": { + "runUrl": "https://github.com/mex-memory/mex/actions/runs/34286120355", + "artifactUrl": "https://github.com/mex-memory/mex/actions/runs/34286120355/artifacts/10079816681", + "pullRequest": 180, + "pullRequestHead": "6e12e6dd34a3bb11735ce3580aece8d37e2e8043", + "schemaVersion": 1, + "kind": "mex-release-benchmark-attempt", + "repositoryHead": "8c046500e2969463014e12dc5844abf76ae403ce", + "github": { + "runId": "34286120355", + "runAttempt": "1", + "sha": "8c046500e2969463014e12dc5844abf76ae403ce" + }, + "runnerAllocation": { + "job": "release_performance_attempt_1", + "runnerName": "GitHub Actions 1000000930", + "runnerOs": "Linux", + "runnerArch": "X64" + }, + "rawReportSha256": "98007786b450eb7a6142d0ce43cf2bb6fc85c287d6e0a8837850318d5924fd04", + "retryRequired": false, + "disposition": "hard_failure" + }, + "environment": { + "os": "ubuntu-24.04", + "platform": "linux", + "architecture": "x64", + "node": "v22.22.0", + "pinnedBudgetEnvironment": true + }, + "formula": "ceil(measured p95 * 1.15)", + "sampleCount": 5, + "previousCalibrationStatus": "calibrated-from-pinned-runs-33005876613-33083122092-33117048710-E33169865368-F33249296778-Goverview", + "unownedBudgetSha256": "7d4c45e5399361ddf3b4292aa33b3ba3e179e0c833547ee4ecc4891bb03c469c", + "profiles": { + "small": { + "samples": [ + 5480288, + 5480372, + 5480384, + 5480384, + 5480428 + ], + "min": 5480288, + "median": 5480384, + "p95": 5480428, + "max": 5480428, + "budgetBytes": 6302493 + }, + "medium": { + "samples": [ + 5474652, + 5474656, + 5480372, + 5480384, + 5482104 + ], + "min": 5474652, + "median": 5480372, + "p95": 5482104, + "max": 5482104, + "budgetBytes": 6304420 + }, + "large": { + "samples": [ + 5480384, + 5480388, + 5481520, + 5482008, + 5486028 + ], + "min": 5480384, + "median": 5481520, + "p95": 5486028, + "max": 5486028, + "budgetBytes": 6308933 + } + }, + "note": "The report completed measurement but failed enforcement because these three Settings budgets were missing. This calibrates only Settings browser heap; all existing limits, confirmation rules and formulas remain frozen. Graph maintenance crossings still require ordinary enforcing CI." +} diff --git a/docs/design/telemetry-performance-results.json b/docs/design/telemetry-performance-results.json new file mode 100644 index 00000000..bff1d91e --- /dev/null +++ b/docs/design/telemetry-performance-results.json @@ -0,0 +1,4499 @@ +{ + "schemaVersion": 1, + "kind": "telemetry-local-performance-characterization", + "generatedAt": "2026-09-09T13:35:22.436Z", + "environment": { + "node": "v22.17.1", + "platform": "darwin", + "release": "25.0.0", + "architecture": "arm64", + "cpu": "Apple M4" + }, + "builds": [ + { + "name": "baseline", + "cliSha256": "3a6d0d2d6682b011b86d71ebe1d5c9eccead4262f53e85743bcf34cc116d4832" + }, + { + "name": "candidate", + "cliSha256": "66719ebce3f290134557cfa62533939093d5fa745ee8708a7e4afa1f03b366ea" + } + ], + "methodology": { + "samplesPerCondition": 20, + "warmupsPerCondition": 5, + "commandArguments": [ + { + "name": "success", + "args": [ + "commands" + ] + }, + { + "name": "failure", + "args": [ + "check", + "--json" + ] + } + ], + "conditionOrder": "rotated every round; baseline/candidate order alternates", + "measuredInterval": "spawn through child close, including loader, CLI, telemetry and natural process cleanup", + "transport": "harness-only preload redirects only the fixed PostHog HTTPS batch destination to real loopback HTTP; all other outbound sockets denied; TLS and external network latency excluded", + "repeatUse": "isolated preseeded installation identity and notice preferences", + "pristineRunsPerCondition": 3, + "lifetimeHangGuardMs": 5000, + "portablePerformanceGate": false, + "quantiles": "Nearest-rank p50/p95; raw samples retain execution order.", + "pairedDifferences": "Each sample subtracts the matching round; p95 of paired differences is distinct from subtracting two p95 values.", + "overheadTargets": { + "captureCallMs": 5, + "flushGraceCapMs": 50, + "status": "Observed capture p95 and flush maximum satisfy these local characterization targets; these are not cross-machine release budgets." + } + }, + "groups": { + "baseline.success.disabled": { + "processMs": { + "samples": [ + 362.41, + 358.193, + 371.673, + 359.724, + 358.176, + 357.403, + 354.806, + 379.359, + 362.426, + 368.501, + 357.496, + 370.951, + 355.841, + 368.307, + 359.098, + 363.083, + 358.165, + 356.168, + 363.233, + 358.339 + ], + "p50": 359.098, + "p95": 371.673, + "min": 354.806, + "max": 379.359 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3672 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.success.healthy": { + "processMs": { + "samples": [ + 369.265, + 367.187, + 369.468, + 366.215, + 367.776, + 368.019, + 372.745, + 373.579, + 368.205, + 366.065, + 369.117, + 368.857, + 372.655, + 367.902, + 383.412, + 369.821, + 368.842, + 370.436, + 367.718, + 372.219 + ], + "p50": 368.857, + "p95": 373.579, + "min": 366.065, + "max": 383.412 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + } + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3672 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "command_run:": 25 + } + }, + "baseline.success.refused": { + "processMs": { + "samples": [ + 366.27, + 370.094, + 367.574, + 379.547, + 368.81, + 370.803, + 364.648, + 367.796, + 365.207, + 370.999, + 369.425, + 365.463, + 378.933, + 371.87, + 366.165, + 365.692, + 377.14, + 381.442, + 366.622, + 371.664 + ], + "p50": 368.81, + "p95": 379.547, + "min": 364.648, + "max": 381.442 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3672 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.success.hanging": { + "processMs": { + "samples": [ + 1025.125, + 1029.003, + 1028.454, + 1029.53, + 1019.748, + 1041.964, + 1041.955, + 1024.062, + 1034.911, + 1033.47, + 1026.647, + 1033.285, + 1031.96, + 1028.656, + 1031.78, + 1028.87, + 1033.056, + 1028.405, + 1041.108, + 1033.703 + ], + "p50": 1029.53, + "p95": 1041.955, + "min": 1019.748, + "max": 1041.964 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + }, + { + "command_run:": 1 + } + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3672 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "command_run:": 25 + } + }, + "baseline.failure.disabled": { + "processMs": { + "samples": [ + 349.439, + 349.505, + 350.601, + 349.51, + 363.477, + 360.276, + 348.896, + 354.077, + 370.83, + 349.534, + 348.042, + 364.436, + 363.603, + 350.949, + 355.422, + 347.082, + 362.865, + 353.988, + 352.612, + 351.548 + ], + "p50": 351.548, + "p95": 364.436, + "min": 347.082, + "max": 370.83 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.failure.healthy": { + "processMs": { + "samples": [ + 348.494, + 348.619, + 350.167, + 349.808, + 348.507, + 352.879, + 352.212, + 350.32, + 351.193, + 365.214, + 354.717, + 351.71, + 349.433, + 355.963, + 347.547, + 349.456, + 348.868, + 349.774, + 356.424, + 353.232 + ], + "p50": 350.167, + "p95": 356.424, + "min": 347.547, + "max": 365.214 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.failure.refused": { + "processMs": { + "samples": [ + 350.563, + 347.52, + 351.879, + 351.615, + 353.991, + 363.443, + 351.888, + 348.884, + 363.017, + 353.045, + 354.988, + 347.511, + 350.717, + 348.388, + 355.657, + 351.444, + 350.307, + 346.58, + 352.82, + 350.881 + ], + "p50": 351.444, + "p95": 363.017, + "min": 346.58, + "max": 363.443 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.failure.hanging": { + "processMs": { + "samples": [ + 369.926, + 356.843, + 353.634, + 352.646, + 358.203, + 346.837, + 350.801, + 355.96, + 351.155, + 349.586, + 349.935, + 369.472, + 356.183, + 363.201, + 346.591, + 351.864, + 361.112, + 351.296, + 352.088, + 348.923 + ], + "p50": 352.088, + "p95": 369.472, + "min": 346.591, + "max": 369.926 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.success.disabled": { + "processMs": { + "samples": [ + 371.941, + 349.263, + 359.335, + 354.259, + 357.033, + 351.393, + 381.427, + 353.781, + 370.784, + 364.901, + 368.309, + 351.588, + 369.128, + 352.005, + 368.16, + 353.658, + 366.437, + 351.668, + 367.127, + 352.952 + ], + "p50": 357.033, + "p95": 371.941, + "min": 349.263, + "max": 381.427 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.success.healthy": { + "processMs": { + "samples": [ + 364.829, + 359.969, + 362.419, + 372.07, + 361.056, + 360.336, + 359.881, + 359.914, + 360.396, + 365.87, + 362.573, + 372.76, + 359.702, + 360.888, + 358.509, + 357.576, + 358.917, + 359.95, + 363.621, + 364.958 + ], + "p50": 360.396, + "p95": 372.07, + "min": 357.576, + "max": 372.76 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "candidate.success.refused": { + "processMs": { + "samples": [ + 359.184, + 364.803, + 363.743, + 364.208, + 361.487, + 356.99, + 360.776, + 365.465, + 360.549, + 363.775, + 361.603, + 359.176, + 360.253, + 361.681, + 359.328, + 374.244, + 357.459, + 444.672, + 359.846, + 359.07 + ], + "p50": 360.776, + "p95": 374.244, + "min": 356.99, + "max": 444.672 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22400, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.success.hanging": { + "processMs": { + "samples": [ + 376.94, + 389.217, + 386.629, + 392.233, + 387.368, + 395.765, + 375.993, + 379.174, + 376.677, + 395.916, + 377.959, + 394.605, + 374.867, + 389.988, + 377.563, + 393.149, + 375.324, + 402.266, + 377.139, + 401.836 + ], + "p50": 386.629, + "p95": 401.836, + "min": 374.867, + "max": 402.266 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:success": 6 + }, + { + "cli.command_started:": 7, + "cli.command_completed:success": 7 + }, + { + "cli.command_started:": 8, + "cli.command_completed:success": 8 + }, + { + "cli.command_started:": 9, + "cli.command_completed:success": 9 + }, + { + "cli.command_started:": 10, + "cli.command_completed:success": 10 + }, + { + "cli.command_started:": 11, + "cli.command_completed:success": 11 + }, + { + "cli.command_started:": 12, + "cli.command_completed:success": 12 + }, + { + "cli.command_started:": 13, + "cli.command_completed:success": 13 + }, + { + "cli.command_started:": 14, + "cli.command_completed:success": 14 + }, + { + "cli.command_started:": 15, + "cli.command_completed:success": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + ] + }, + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22400, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + }, + "candidate.failure.disabled": { + "processMs": { + "samples": [ + 351.544, + 351.692, + 351.907, + 352.923, + 351.07, + 351.24, + 348.805, + 351.057, + 351.435, + 353.82, + 355.326, + 352.845, + 358.988, + 364.01, + 353.479, + 365.605, + 358.634, + 350.073, + 351.171, + 354.3 + ], + "p50": 351.907, + "p95": 364.01, + "min": 348.805, + "max": 365.605 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.failure.healthy": { + "processMs": { + "samples": [ + 359.458, + 369.714, + 361.999, + 359.445, + 360.66, + 358.36, + 364.692, + 358.863, + 361.25, + 360.146, + 362.149, + 359.033, + 359.962, + 372.446, + 358.985, + 356.267, + 359.87, + 359.114, + 363.329, + 376.76 + ], + "p50": 359.962, + "p95": 372.446, + "min": 356.267, + "max": 376.76 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:failure": 1 + } + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + } + }, + "candidate.failure.refused": { + "processMs": { + "samples": [ + 364.267, + 369.547, + 361.693, + 357.14, + 371.899, + 360.18, + 374.066, + 359.741, + 361.145, + 356.6, + 358.709, + 358.266, + 359.994, + 360.965, + 359.256, + 358.979, + 362.363, + 364.98, + 375.809, + 360.677 + ], + "p50": 360.677, + "p95": 374.066, + "min": 356.6, + "max": 375.809 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22250, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.failure.hanging": { + "processMs": { + "samples": [ + 373.44, + 389.113, + 379.181, + 378.198, + 377.171, + 369.636, + 377.562, + 373.853, + 374.655, + 378.843, + 377.619, + 374.173, + 380.015, + 372.743, + 379.189, + 377.435, + 373.696, + 381.673, + 374.801, + 375.492 + ], + "p50": 377.171, + "p95": 381.673, + "min": 369.636, + "max": 389.113 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:failure": 6 + }, + { + "cli.command_started:": 7, + "cli.command_completed:failure": 7 + }, + { + "cli.command_started:": 8, + "cli.command_completed:failure": 8 + }, + { + "cli.command_started:": 9, + "cli.command_completed:failure": 9 + }, + { + "cli.command_started:": 10, + "cli.command_completed:failure": 10 + }, + { + "cli.command_started:": 11, + "cli.command_completed:failure": 11 + }, + { + "cli.command_started:": 12, + "cli.command_completed:failure": 12 + }, + { + "cli.command_started:": 13, + "cli.command_completed:failure": 13 + }, + { + "cli.command_started:": 14, + "cli.command_completed:failure": 14 + }, + { + "cli.command_started:": 15, + "cli.command_completed:failure": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + ] + }, + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 0 + ], + "stderrBytes": [ + 56 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22250, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + } + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + } + }, + "pairedDeltasMs": { + "baseline.success.healthy-disabled": { + "samples": [ + 6.855, + 8.994, + -2.205, + 6.49, + 9.6, + 10.616, + 17.939, + -5.78, + 5.78, + -2.436, + 11.621, + -2.094, + 16.813, + -0.405, + 24.315, + 6.738, + 10.677, + 14.269, + 4.485, + 13.88 + ], + "p50": 6.855, + "p95": 17.939, + "min": -5.78, + "max": 24.315 + }, + "baseline.success.refused-disabled": { + "samples": [ + 3.86, + 11.901, + -4.099, + 19.823, + 10.634, + 13.4, + 9.842, + -11.563, + 2.781, + 2.499, + 11.929, + -5.488, + 23.092, + 3.564, + 7.067, + 2.609, + 18.976, + 25.274, + 3.388, + 13.325 + ], + "p50": 7.067, + "p95": 23.092, + "min": -11.563, + "max": 25.274 + }, + "baseline.success.hanging-disabled": { + "samples": [ + 662.715, + 670.81, + 656.78, + 669.806, + 661.571, + 684.561, + 687.149, + 644.702, + 672.485, + 664.969, + 669.151, + 662.334, + 676.119, + 660.35, + 672.683, + 665.787, + 674.892, + 672.237, + 677.875, + 675.364 + ], + "p50": 669.806, + "p95": 684.561, + "min": 644.702, + "max": 687.149 + }, + "baseline.failure.healthy-disabled": { + "samples": [ + -0.945, + -0.886, + -0.434, + 0.298, + -14.969, + -7.397, + 3.316, + -3.758, + -19.637, + 15.68, + 6.675, + -12.726, + -14.17, + 5.015, + -7.875, + 2.373, + -13.997, + -4.214, + 3.811, + 1.684 + ], + "p50": -0.945, + "p95": 6.675, + "min": -19.637, + "max": 15.68 + }, + "baseline.failure.refused-disabled": { + "samples": [ + 1.123, + -1.985, + 1.278, + 2.106, + -9.486, + 3.167, + 2.992, + -5.193, + -7.813, + 3.51, + 6.947, + -16.925, + -12.887, + -2.561, + 0.235, + 4.361, + -12.558, + -7.408, + 0.207, + -0.667 + ], + "p50": -0.667, + "p95": 4.361, + "min": -16.925, + "max": 6.947 + }, + "baseline.failure.hanging-disabled": { + "samples": [ + 20.486, + 7.337, + 3.034, + 3.137, + -5.274, + -13.439, + 1.905, + 1.883, + -19.674, + 0.052, + 1.893, + 5.036, + -7.421, + 12.252, + -8.83, + 4.782, + -1.754, + -2.692, + -0.524, + -2.625 + ], + "p50": 0.052, + "p95": 12.252, + "min": -19.674, + "max": 20.486 + }, + "candidate.success.healthy-disabled": { + "samples": [ + -7.112, + 10.705, + 3.084, + 17.811, + 4.023, + 8.943, + -21.546, + 6.132, + -10.388, + 0.97, + -5.736, + 21.171, + -9.426, + 8.883, + -9.651, + 3.918, + -7.52, + 8.282, + -3.506, + 12.006 + ], + "p50": 3.084, + "p95": 17.811, + "min": -21.546, + "max": 21.171 + }, + "candidate.success.refused-disabled": { + "samples": [ + -12.757, + 15.54, + 4.408, + 9.949, + 4.455, + 5.598, + -20.651, + 11.684, + -10.235, + -1.126, + -6.706, + 7.588, + -8.875, + 9.676, + -8.831, + 20.586, + -8.978, + 93.004, + -7.282, + 6.118 + ], + "p50": 4.408, + "p95": 20.586, + "min": -20.651, + "max": 93.004 + }, + "candidate.success.hanging-disabled": { + "samples": [ + 4.998, + 39.953, + 27.294, + 37.974, + 30.335, + 44.372, + -5.434, + 25.393, + 5.893, + 31.016, + 9.649, + 43.016, + 5.739, + 37.983, + 9.404, + 39.491, + 8.887, + 50.598, + 10.012, + 48.884 + ], + "p50": 27.294, + "p95": 48.884, + "min": -5.434, + "max": 50.598 + }, + "candidate.failure.healthy-disabled": { + "samples": [ + 7.914, + 18.022, + 10.091, + 6.523, + 9.59, + 7.12, + 15.887, + 7.806, + 9.815, + 6.325, + 6.823, + 6.188, + 0.974, + 8.436, + 5.506, + -9.338, + 1.235, + 9.041, + 12.158, + 22.46 + ], + "p50": 7.806, + "p95": 18.022, + "min": -9.338, + "max": 22.46 + }, + "candidate.failure.refused-disabled": { + "samples": [ + 12.723, + 17.855, + 9.785, + 4.218, + 20.829, + 8.94, + 25.261, + 8.685, + 9.71, + 2.779, + 3.384, + 5.42, + 1.005, + -3.045, + 5.778, + -6.626, + 3.729, + 14.908, + 24.638, + 6.377 + ], + "p50": 6.377, + "p95": 24.638, + "min": -6.626, + "max": 25.261 + }, + "candidate.failure.hanging-disabled": { + "samples": [ + 21.895, + 37.421, + 27.274, + 25.275, + 26.101, + 18.396, + 28.756, + 22.797, + 23.22, + 25.023, + 22.294, + 21.328, + 21.027, + 8.733, + 25.71, + 11.83, + 15.061, + 31.6, + 23.63, + 21.191 + ], + "p50": 22.797, + "p95": 31.6, + "min": 8.733, + "max": 37.421 + }, + "candidate-baseline.success.disabled": { + "samples": [ + 9.531, + -8.93, + -12.338, + -5.466, + -1.143, + -6.01, + 26.621, + -25.578, + 8.358, + -3.6, + 10.813, + -19.362, + 13.287, + -16.301, + 9.062, + -9.425, + 8.273, + -4.499, + 3.894, + -5.387 + ], + "p50": -4.499, + "p95": 13.287, + "min": -25.578, + "max": 26.621 + }, + "candidate-baseline.success.healthy": { + "samples": [ + -4.436, + -7.219, + -7.049, + 5.855, + -6.72, + -7.684, + -12.863, + -13.665, + -7.81, + -0.195, + -6.544, + 3.903, + -12.953, + -7.014, + -24.903, + -12.245, + -9.924, + -10.486, + -4.097, + -7.261 + ], + "p50": -7.261, + "p95": 3.903, + "min": -24.903, + "max": 5.855 + }, + "candidate-baseline.success.refused": { + "samples": [ + -7.086, + -5.29, + -3.832, + -15.34, + -7.323, + -13.812, + -3.872, + -2.331, + -4.658, + -7.224, + -7.822, + -6.287, + -18.68, + -10.189, + -6.836, + 8.553, + -19.681, + 63.23, + -6.776, + -12.594 + ], + "p50": -7.086, + "p95": 8.553, + "min": -19.681, + "max": 63.23 + }, + "candidate-baseline.success.hanging": { + "samples": [ + -648.185, + -639.786, + -641.824, + -637.297, + -632.38, + -646.199, + -665.962, + -644.888, + -658.234, + -637.554, + -648.688, + -638.681, + -657.093, + -638.668, + -654.217, + -635.721, + -657.732, + -626.139, + -663.969, + -631.867 + ], + "p50": -644.888, + "p95": -631.867, + "min": -665.962, + "max": -626.139 + }, + "candidate-baseline.failure.disabled": { + "samples": [ + 2.105, + 2.187, + 1.307, + 3.413, + -12.406, + -9.036, + -0.091, + -3.02, + -19.395, + 4.286, + 7.284, + -11.591, + -4.615, + 13.061, + -1.943, + 18.523, + -4.231, + -3.916, + -1.442, + 2.753 + ], + "p50": -1.442, + "p95": 13.061, + "min": -19.395, + "max": 18.523 + }, + "candidate-baseline.failure.healthy": { + "samples": [ + 10.964, + 21.095, + 11.832, + 9.638, + 12.153, + 5.481, + 12.481, + 8.543, + 10.057, + -5.068, + 7.432, + 7.323, + 10.529, + 16.483, + 11.438, + 6.812, + 11.001, + 9.339, + 6.905, + 23.529 + ], + "p50": 10.057, + "p95": 21.095, + "min": -5.068, + "max": 23.529 + }, + "candidate-baseline.failure.refused": { + "samples": [ + 13.705, + 22.027, + 9.814, + 5.525, + 17.909, + -3.263, + 22.178, + 10.857, + -1.871, + 3.555, + 3.721, + 10.755, + 9.277, + 12.577, + 3.599, + 7.535, + 12.056, + 18.4, + 22.989, + 9.797 + ], + "p50": 9.814, + "p95": 22.178, + "min": -3.263, + "max": 22.989 + }, + "candidate-baseline.failure.hanging": { + "samples": [ + 3.514, + 32.271, + 25.547, + 25.551, + 18.968, + 22.799, + 26.761, + 17.893, + 23.5, + 29.257, + 27.684, + 4.701, + 23.833, + 9.542, + 32.597, + 25.571, + 12.584, + 30.377, + 22.712, + 26.569 + ], + "p50": 23.833, + "p95": 32.271, + "min": 3.514, + "max": 32.597 + } + }, + "pristine": [ + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 357.031, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 372.936, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [ + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "command_run:": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 351.751, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 361.629, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 351.168, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 363.231, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 363.139, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 381.691, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [ + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "command_run:": 1 + } + }, + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 366.359, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 369.855, + "exitCode": 0, + "stdoutBytes": 3672, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [ + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "command_run:": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 352.129, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 363.715, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + } + ], + "moduleProbe": { + "methodology": "Separately bundled current internal telemetry module; direct synchronous capture-call and awaited flush timings. Module/compiler startup excluded. Each repetition resets transport handles and keeps its isolated durable queue; first capture includes queue initialization. This does not replace built-CLI lifetime measurements.", + "bundleSha256": "592f98a3406bd0238400134f5421bebfd83b2ee209e8c2549777e8aac982b751", + "conditions": { + "disabled": { + "captureStartedMs": { + "samples": [ + 0.002, + 0.002, + 0.001, + 0.001, + 0.003, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.002, + "min": 0.001, + "max": 0.003 + }, + "captureCompletedMs": { + "samples": [ + 0.003, + 0.002, + 0.001, + 0.001, + 0.002, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.002, + "min": 0.001, + "max": 0.003 + }, + "flushMs": { + "samples": [ + 0.001, + 0.001, + 0.001, + 0.043, + 0.005, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.005, + "min": 0.001, + "max": 0.043 + }, + "firstCaptureStartedMs": 0.096, + "queue": { + "state": "absent", + "events": 0, + "bytes": 0 + }, + "requests": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0 + }, + "healthy": { + "captureStartedMs": { + "samples": [ + 0.504, + 0.68, + 0.577, + 0.647, + 0.511, + 0.483, + 0.576, + 0.499, + 0.704, + 0.543, + 0.602, + 0.488, + 0.508, + 0.46, + 0.669, + 0.527, + 0.553, + 0.565, + 0.487, + 0.466 + ], + "p50": 0.527, + "p95": 0.68, + "min": 0.46, + "max": 0.704 + }, + "captureCompletedMs": { + "samples": [ + 0.375, + 0.427, + 0.408, + 0.408, + 0.376, + 0.398, + 0.392, + 0.374, + 0.428, + 0.391, + 0.42, + 0.36, + 0.37, + 0.357, + 0.438, + 0.372, + 0.388, + 0.367, + 0.366, + 0.37 + ], + "p50": 0.376, + "p95": 0.428, + "min": 0.357, + "max": 0.438 + }, + "flushMs": { + "samples": [ + 1.352, + 1.325, + 1.427, + 1.992, + 1.069, + 1.063, + 1.102, + 1.199, + 1.177, + 1.205, + 1.099, + 0.917, + 0.883, + 0.916, + 1.334, + 1.265, + 1.099, + 0.969, + 0.925, + 0.959 + ], + "p50": 1.099, + "p95": 1.427, + "min": 0.883, + "max": 1.992 + }, + "firstCaptureStartedMs": 3.21, + "queue": { + "state": "available", + "events": 0, + "bytes": 0 + }, + "requests": 25, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 25 + }, + "refused": { + "captureStartedMs": { + "samples": [ + 0.505, + 0.528, + 0.553, + 0.617, + 0.527, + 0.493, + 0.486, + 0.547, + 0.585, + 0.488, + 0.677, + 0.681, + 0.586, + 0.508, + 0.505, + 0.542, + 0.502, + 0.567, + 0.606, + 0.506 + ], + "p50": 0.528, + "p95": 0.677, + "min": 0.486, + "max": 0.681 + }, + "captureCompletedMs": { + "samples": [ + 0.426, + 0.381, + 0.383, + 0.383, + 0.36, + 0.373, + 0.359, + 0.363, + 0.402, + 0.373, + 0.503, + 0.51, + 0.376, + 0.396, + 0.383, + 0.4, + 0.378, + 0.385, + 0.425, + 0.377 + ], + "p50": 0.383, + "p95": 0.503, + "min": 0.359, + "max": 0.51 + }, + "flushMs": { + "samples": [ + 0.994, + 1.28, + 1.376, + 1.895, + 1.216, + 1.186, + 1.075, + 1.278, + 1.287, + 1.435, + 1.532, + 1.351, + 1.146, + 1.204, + 1.291, + 1.282, + 1.183, + 1.434, + 1.181, + 1.122 + ], + "p50": 1.278, + "p95": 1.532, + "min": 0.994, + "max": 1.895 + }, + "firstCaptureStartedMs": 2.943, + "queue": { + "state": "available", + "events": 50, + "bytes": 22400 + }, + "requests": 0, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 25, + "responseCompletions": 0 + }, + "hanging": { + "captureStartedMs": { + "samples": [ + 1.126, + 1.42, + 1.419, + 1.912, + 1.683, + 1.744, + 2.568, + 2.119, + 1.833, + 1.643, + 2.079, + 2.08, + 1.911, + 2.05, + 2.41, + 1.573, + 1.417, + 2.011, + 1.947, + 2.082 + ], + "p50": 1.911, + "p95": 2.41, + "min": 1.126, + "max": 2.568 + }, + "captureCompletedMs": { + "samples": [ + 0.773, + 0.775, + 0.889, + 0.97, + 0.925, + 0.932, + 1.227, + 1.747, + 1.328, + 1.292, + 1.075, + 1.364, + 1.861, + 1.486, + 1.466, + 0.9, + 0.879, + 0.986, + 1.383, + 1.282 + ], + "p50": 1.075, + "p95": 1.747, + "min": 0.773, + "max": 1.861 + }, + "flushMs": { + "samples": [ + 26.198, + 28.279, + 27.975, + 28.356, + 27.915, + 28.268, + 27.006, + 27.365, + 25.831, + 27.025, + 27.085, + 28.019, + 27.445, + 27.542, + 28.312, + 27.45, + 28.072, + 29.114, + 27.12, + 27.869 + ], + "p50": 27.542, + "p95": 28.356, + "min": 25.831, + "max": 29.114 + }, + "firstCaptureStartedMs": 3.011, + "queue": { + "state": "available", + "events": 50, + "bytes": 22400 + }, + "requests": 25, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 25, + "responseCompletions": 0 + } + } + }, + "limitations": [ + "Single local host; paired differences include scheduler and module-loading noise.", + "Server receipt is not client acknowledgement: hanging ingestion receives bodies but never acknowledges them. Client HTTP response completions and durable retained events are reported separately; receipt/retention accounting deduplicates UUIDs in memory and retains no identifiers.", + "Local HTTP ingestion does not measure production TLS or Internet latency.", + "Candidate offline queues accumulate across the repeated invocations; the queue remains bounded and is inspected only after timed runs.", + "First-command runs are bounded observations, not a statistically stable p95.", + "This command scenario accumulates up to 50 queued events. Maximum-cap queue performance and production network delivery rates are not measured here.", + "The wrapper measures real built-CLI main execution but adds the same preload instrumentation to baseline/candidate and enabled/disabled runs. Embedded Yoga WASM data-URL loading is allowed because it performs no network I/O." + ], + "provenance": { + "baselineCommit": "d64f171054e09149bf79790a91a0077a07975828", + "candidate": "working-tree telemetry implementation on codex/0.8.1-telemetry; built CLI SHA-256 is recorded above", + "rawReportSha256": "05520b33b65449120a76d0eccf9621165c790e1f625fd3bd287a54d1fd8ea023", + "harnessSha256": "857bb371755accb0cbd4567b4e3b1c58f91778c0f3480fa8f2762c883d38de10", + "rawReportBytes": 244536, + "recording": "Full raw timing and paired-difference samples retained here; repeated server event bodies are replaced by per-run closed event counts. No identifiers, tokens or absolute local paths are retained." + }, + "findings": { + "candidateEnabledMinusDisabledMs": { + "success": { + "healthy": { + "p50": 3.084, + "p95": 17.811 + }, + "refused": { + "p50": 4.408, + "p95": 20.586 + }, + "hanging": { + "p50": 27.294, + "p95": 48.884 + } + }, + "failure": { + "healthy": { + "p50": 7.806, + "p95": 18.022 + }, + "refused": { + "p50": 6.377, + "p95": 24.638 + }, + "hanging": { + "p50": 22.797, + "p95": 31.6 + } + } + }, + "candidateMinusBaselineMs": { + "success": { + "disabled": { + "p50": -4.499, + "p95": 13.287 + }, + "healthy": { + "p50": -7.261, + "p95": 3.903 + }, + "refused": { + "p50": -7.086, + "p95": 8.553 + }, + "hanging": { + "p50": -644.888, + "p95": -631.867 + } + }, + "failure": { + "disabled": { + "p50": -1.442, + "p95": 13.061 + }, + "healthy": { + "p50": 10.057, + "p95": 21.095 + }, + "refused": { + "p50": 9.814, + "p95": 22.178 + }, + "hanging": { + "p50": 23.833, + "p95": 32.271 + } + } + }, + "captureCallP95MaxMs": 2.41, + "firstCaptureMaxMs": 3.21, + "flushObservedMaxMs": 29.114, + "healthyOutcomeEvidence": "For each candidate command, all 25 started and 25 completed events reached local ingestion across warmups and measured runs; the queue was empty afterward. Both success and failure outcomes were present. The old failure path sent no event.", + "offlineEvidence": "Refused and hanging candidate conditions retained all 50 events per command in a 45,056-byte SQLite store with zero active claims at final inspection. Hanging server receipts were unacknowledged and remain queued; UUID-union accounting avoids double counting.", + "isolationAndCleanup": "All disabled runs made zero telemetry requests. All child processes exited naturally with expected output/exit code; no server sockets remained after exit. Disabled pristine homes stayed empty.", + "failureCost": "The new failure path now records and attempts delivery of an outcome. Its extra healthy median cost was about 10ms versus the old failure path, which exited without sending an event." + } +} diff --git a/docs/design/telemetry-project-context-performance.json b/docs/design/telemetry-project-context-performance.json new file mode 100644 index 00000000..58e31d23 --- /dev/null +++ b/docs/design/telemetry-project-context-performance.json @@ -0,0 +1,5388 @@ +{ + "schemaVersion": 1, + "kind": "telemetry-local-performance-characterization", + "generatedAt": "2026-09-09T16:11:53.602Z", + "environment": { + "node": "v22.17.1", + "platform": "darwin", + "release": "25.0.0", + "architecture": "arm64", + "cpu": "Apple M4" + }, + "builds": [ + { + "name": "baseline", + "cliSha256": "66719ebce3f290134557cfa62533939093d5fa745ee8708a7e4afa1f03b366ea" + }, + { + "name": "candidate", + "cliSha256": "99ebd1b40c434de47bcfdbe5db74af43a5be27afa84c96d0bc401cbdb52e067b" + } + ], + "methodology": { + "samplesPerCondition": 20, + "warmupsPerCondition": 5, + "commandArguments": [ + { + "name": "success", + "args": [ + "commands" + ] + }, + { + "name": "failure", + "args": [ + "check", + "--json" + ] + } + ], + "conditionOrder": "rotated every round; baseline/candidate order alternates", + "measuredInterval": "spawn through child close, including loader, CLI, telemetry and natural process cleanup", + "transport": "harness-only preload redirects only the fixed PostHog HTTPS batch destination to real loopback HTTP; all other outbound sockets denied; TLS and external network latency excluded", + "repeatUse": "isolated preseeded installation identity and notice preferences", + "pristineRunsPerCondition": 3, + "lifetimeHangGuardMs": 5000, + "portablePerformanceGate": false, + "quantiles": "Nearest-rank p50/p95; raw samples retain execution order.", + "pairedDifferences": "Each sample subtracts the matching round; p95 of paired differences is distinct from subtracting two p95 values.", + "overheadTargets": { + "captureCallMs": 5, + "flushGraceCapMs": 50, + "status": "Observed capture p95 and flush maximum satisfy these local characterization targets; these are not cross-machine release budgets." + } + }, + "groups": { + "baseline.success.disabled": { + "processMs": { + "samples": [ + 381.749, + 394.218, + 374.875, + 468.532, + 379.186, + 379.463, + 392.565, + 454.199, + 420.837, + 388.333, + 413.637, + 479.133, + 524.278, + 450.638, + 437.832, + 588.772, + 434.912, + 473.379, + 452.505, + 552.229 + ], + "p50": 434.912, + "p95": 552.229, + "min": 374.875, + "max": 588.772 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.success.healthy": { + "processMs": { + "samples": [ + 397.438, + 391.827, + 398.701, + 382.17, + 386.164, + 387.949, + 421.567, + 401.214, + 432.534, + 387.22, + 397.061, + 435.098, + 415.658, + 473.833, + 443.237, + 502.709, + 454.669, + 491.207, + 470.762, + 482.265 + ], + "p50": 415.658, + "p95": 491.207, + "min": 382.17, + "max": 502.709 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + ] + }, + "projectContextEventsVerified": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 0 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "baseline.success.refused": { + "processMs": { + "samples": [ + 387.337, + 483.46, + 387.895, + 414.843, + 383.716, + 474.494, + 394.499, + 396.054, + 426.897, + 481.065, + 413.872, + 452.867, + 510.372, + 507.892, + 482.983, + 512.488, + 452.282, + 519.586, + 458.725, + 526.762 + ], + "p50": 452.867, + "p95": 519.586, + "min": 383.716, + "max": 526.762 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22400, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.success.hanging": { + "processMs": { + "samples": [ + 398.285, + 415.227, + 397.409, + 421.915, + 398.69, + 400.816, + 405.382, + 412.766, + 430.855, + 412.939, + 424.385, + 477.347, + 546.02, + 450.868, + 458.808, + 497.993, + 480.399, + 468.292, + 466.353, + 533.278 + ], + "p50": 424.385, + "p95": 533.278, + "min": 397.409, + "max": 546.02 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:success": 6 + }, + { + "cli.command_started:": 7, + "cli.command_completed:success": 7 + }, + { + "cli.command_started:": 8, + "cli.command_completed:success": 8 + }, + { + "cli.command_started:": 9, + "cli.command_completed:success": 9 + }, + { + "cli.command_started:": 10, + "cli.command_completed:success": 10 + }, + { + "cli.command_started:": 11, + "cli.command_completed:success": 11 + }, + { + "cli.command_started:": 12, + "cli.command_completed:success": 12 + }, + { + "cli.command_started:": 13, + "cli.command_completed:success": 13 + }, + { + "cli.command_started:": 14, + "cli.command_completed:success": 14 + }, + { + "cli.command_started:": 15, + "cli.command_completed:success": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + ] + }, + "projectContextEventsVerified": [ + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22400, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + }, + "baseline.failure.disabled": { + "processMs": { + "samples": [ + 544.698, + 542.347, + 551.372, + 472.746, + 572.727, + 548.02, + 558.014, + 468.583, + 551.719, + 546.939, + 598.065, + 531.194, + 584.268, + 622.984, + 615.705, + 540.988, + 590.61, + 596.142, + 613.101, + 634.572 + ], + "p50": 551.719, + "p95": 622.984, + "min": 468.583, + "max": 634.572 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.failure.healthy": { + "processMs": { + "samples": [ + 557.134, + 540.648, + 565.097, + 562.131, + 550.465, + 568.89, + 555.445, + 576.626, + 561.079, + 557.804, + 576.975, + 611.5, + 658.519, + 632.546, + 624.525, + 642.278, + 604.178, + 610.791, + 610.765, + 651.473 + ], + "p50": 576.626, + "p95": 651.473, + "min": 540.648, + "max": 658.519 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + } + ] + }, + "projectContextEventsVerified": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 1, + "payloadBytes": 465, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": { + "cli.command_completed:failure": 1 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 1 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:failure": 24 + } + }, + "baseline.failure.refused": { + "processMs": { + "samples": [ + 553.14, + 464.352, + 546.196, + 549.468, + 565.627, + 481.802, + 562.028, + 570.894, + 574.741, + 483.371, + 597.59, + 604.882, + 614.221, + 647.847, + 625.996, + 614.221, + 599.098, + 576.272, + 622.939, + 618.127 + ], + "p50": 574.741, + "p95": 625.996, + "min": 464.352, + "max": 647.847 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22275, + "activeClaims": 0, + "databaseBytes": 45056, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "baseline.failure.hanging": { + "processMs": { + "samples": [ + 572.593, + 542.509, + 537.749, + 545.982, + 569.749, + 546.688, + 551.644, + 547.447, + 556.141, + 557.095, + 583.786, + 591.709, + 597.164, + 682.857, + 660.847, + 643.066, + 595.13, + 641.491, + 628.421, + 630.954 + ], + "p50": 572.593, + "p95": 660.847, + "min": 537.749, + "max": 682.857 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:failure": 5 + }, + { + "cli.command_started:": 7, + "cli.command_completed:failure": 6 + }, + { + "cli.command_started:": 8, + "cli.command_completed:failure": 7 + }, + { + "cli.command_started:": 9, + "cli.command_completed:failure": 8 + }, + { + "cli.command_started:": 10, + "cli.command_completed:failure": 9 + }, + { + "cli.command_started:": 11, + "cli.command_completed:failure": 10 + }, + { + "cli.command_started:": 12, + "cli.command_completed:failure": 11 + }, + { + "cli.command_started:": 13, + "cli.command_completed:failure": 12 + }, + { + "cli.command_started:": 14, + "cli.command_completed:failure": 13 + }, + { + "cli.command_started:": 15, + "cli.command_completed:failure": 14 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + ] + }, + "projectContextEventsVerified": [ + 11, + 13, + 15, + 17, + 19, + 21, + 23, + 25, + 27, + 29, + 31, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 22275, + "activeClaims": 0, + "databaseBytes": 40960, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + }, + "candidate.success.disabled": { + "processMs": { + "samples": [ + 369.725, + 375.531, + 370.592, + 365.959, + 370.085, + 379.972, + 396.824, + 388.924, + 398.117, + 380.688, + 386.893, + 400.853, + 539.292, + 431.809, + 425.703, + 483.518, + 431.409, + 469.802, + 444.094, + 454.043 + ], + "p50": 396.824, + "p95": 483.518, + "min": 365.959, + "max": 539.292 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.success.healthy": { + "processMs": { + "samples": [ + 455.275, + 377.021, + 382.484, + 468.332, + 454.433, + 395.844, + 397.693, + 388.31, + 464.87, + 398.265, + 410.155, + 413.989, + 501.555, + 504.164, + 435.089, + 634.303, + 530.903, + 483.663, + 455.903, + 477.22 + ], + "p50": 454.433, + "p95": 530.903, + "min": 377.021, + "max": 634.303 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + }, + { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + ] + }, + "projectContextEventsVerified": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 0 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "candidate.success.refused": { + "processMs": { + "samples": [ + 386.837, + 383.642, + 385.957, + 412.05, + 379.182, + 384.703, + 383.893, + 392.641, + 400.208, + 403.237, + 401.074, + 439.551, + 438.504, + 422.089, + 452.842, + 489.627, + 443.387, + 436.968, + 461.486, + 515.287 + ], + "p50": 403.237, + "p95": 489.627, + "min": 379.182, + "max": 515.287 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 27100, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.success.hanging": { + "processMs": { + "samples": [ + 399.589, + 398.279, + 474.571, + 406.769, + 406.761, + 407.675, + 497.89, + 422.899, + 409.437, + 420.913, + 473.806, + 473.44, + 497.039, + 450.138, + 525.614, + 479.444, + 451.571, + 496.208, + 554.965, + 523.952 + ], + "p50": 451.571, + "p95": 525.614, + "min": 398.279, + "max": 554.965 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:success": 6 + }, + { + "cli.command_started:": 7, + "cli.command_completed:success": 7 + }, + { + "cli.command_started:": 8, + "cli.command_completed:success": 8 + }, + { + "cli.command_started:": 9, + "cli.command_completed:success": 9 + }, + { + "cli.command_started:": 10, + "cli.command_completed:success": 10 + }, + { + "cli.command_started:": 11, + "cli.command_completed:success": 11 + }, + { + "cli.command_started:": 12, + "cli.command_completed:success": 12 + }, + { + "cli.command_started:": 13, + "cli.command_completed:success": 13 + }, + { + "cli.command_started:": 14, + "cli.command_completed:success": 14 + }, + { + "cli.command_started:": 15, + "cli.command_completed:success": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + ] + }, + "projectContextEventsVerified": [ + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32 + ], + "output": { + "exitCodes": [ + 0 + ], + "stdoutBytes": [ + 3674 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 27100, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:success": 16 + } + }, + "candidate.failure.disabled": { + "processMs": { + "samples": [ + 543.24, + 546.425, + 548.122, + 564.943, + 533.326, + 560.594, + 551.136, + 546.488, + 562.734, + 543.658, + 576.253, + 594.907, + 578.136, + 601.024, + 607.694, + 630.521, + 583.971, + 596.019, + 604.316, + 648.465 + ], + "p50": 564.943, + "p95": 630.521, + "min": 533.326, + "max": 648.465 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id" + ], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.failure.healthy": { + "processMs": { + "samples": [ + 458.952, + 540.531, + 545.565, + 548.27, + 471.999, + 546.208, + 548.082, + 549.491, + 482.773, + 563.361, + 671.361, + 590.861, + 585.543, + 603.779, + 624.665, + 597.03, + 526.566, + 606.6, + 622.41, + 625.81 + ], + "p50": 549.491, + "p95": 625.81, + "min": 458.952, + "max": 671.361 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "clientHttpResponseCompletions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + }, + { + "cli.command_completed:failure": 1, + "cli.command_started:": 1 + } + ] + }, + "projectContextEventsVerified": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 1, + "payloadBytes": 559, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": { + "cli.command_completed:failure": 1 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 1 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:failure": 24 + } + }, + "candidate.failure.refused": { + "processMs": { + "samples": [ + 548.887, + 545.376, + 545.327, + 540.487, + 552.872, + 541.109, + 549.918, + 546.458, + 570.042, + 552.413, + 581.315, + 572.775, + 625.763, + 719.889, + 620.848, + 621.681, + 604.701, + 634.786, + 598.408, + 606.254 + ], + "p50": 570.042, + "p95": 634.786, + "min": 540.487, + "max": 719.889 + }, + "delivery": { + "serverReceivedRequests": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} + ] + }, + "projectContextEventsVerified": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 26975, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": {} + }, + "candidate.failure.hanging": { + "processMs": { + "samples": [ + 556.562, + 541.36, + 466.252, + 543.539, + 547.378, + 564.167, + 465.861, + 545.439, + 559.561, + 554.296, + 498.991, + 583.034, + 597.477, + 670.665, + 542.954, + 618.729, + 591.661, + 598.53, + 557.146, + 610.378 + ], + "p50": 556.562, + "p95": 618.729, + "min": 465.861, + "max": 670.665 + }, + "delivery": { + "serverReceivedRequests": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "requestErrors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "clientHttpResponseCompletions": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "openSocketsAfterExit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "serverReceivedEventCountsPerRun": [ + { + "cli.command_started:": 6, + "cli.command_completed:failure": 5 + }, + { + "cli.command_started:": 7, + "cli.command_completed:failure": 6 + }, + { + "cli.command_started:": 8, + "cli.command_completed:failure": 7 + }, + { + "cli.command_started:": 9, + "cli.command_completed:failure": 8 + }, + { + "cli.command_started:": 10, + "cli.command_completed:failure": 9 + }, + { + "cli.command_started:": 11, + "cli.command_completed:failure": 10 + }, + { + "cli.command_started:": 12, + "cli.command_completed:failure": 11 + }, + { + "cli.command_started:": 13, + "cli.command_completed:failure": 12 + }, + { + "cli.command_started:": 14, + "cli.command_completed:failure": 13 + }, + { + "cli.command_started:": 15, + "cli.command_completed:failure": 14 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 15 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + }, + { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + ] + }, + "projectContextEventsVerified": [ + 11, + 13, + 15, + 17, + 19, + 21, + 23, + 25, + 27, + 29, + 31, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32 + ], + "output": { + "exitCodes": [ + 1 + ], + "stdoutBytes": [ + 2031 + ], + "stderrBytes": [ + 0 + ] + }, + "files": [ + ".mex/config.json", + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 50, + "payloadBytes": 26975, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + }, + "projectContextEventsVerified": 50 + }, + "uniqueServerReceivedEventsIncludingWarmups": { + "cli.command_started:": 16, + "cli.command_completed:failure": 16 + } + } + }, + "pairedDeltasMs": { + "baseline.success.healthy-disabled": { + "samples": [ + 15.689, + -2.392, + 23.827, + -86.362, + 6.978, + 8.486, + 29.002, + -52.985, + 11.697, + -1.112, + -16.576, + -44.035, + -108.62, + 23.195, + 5.405, + -86.063, + 19.757, + 17.828, + 18.256, + -69.964 + ], + "p50": 5.405, + "p95": 23.827, + "min": -108.62, + "max": 29.002 + }, + "baseline.success.refused-disabled": { + "samples": [ + 5.588, + 89.242, + 13.021, + -53.689, + 4.53, + 95.031, + 1.935, + -58.145, + 6.06, + 92.732, + 0.236, + -26.265, + -13.906, + 57.254, + 45.151, + -76.284, + 17.37, + 46.207, + 6.22, + -25.466 + ], + "p50": 5.588, + "p95": 92.732, + "min": -76.284, + "max": 95.031 + }, + "baseline.success.hanging-disabled": { + "samples": [ + 16.536, + 21.009, + 22.535, + -46.617, + 19.504, + 21.353, + 12.818, + -41.434, + 10.018, + 24.606, + 10.748, + -1.785, + 21.742, + 0.23, + 20.976, + -90.778, + 45.488, + -5.087, + 13.848, + -18.95 + ], + "p50": 12.818, + "p95": 24.606, + "min": -90.778, + "max": 45.488 + }, + "baseline.failure.healthy-disabled": { + "samples": [ + 12.436, + -1.699, + 13.725, + 89.384, + -22.262, + 20.869, + -2.569, + 108.043, + 9.36, + 10.865, + -21.09, + 80.306, + 74.25, + 9.562, + 8.82, + 101.29, + 13.568, + 14.648, + -2.336, + 16.901 + ], + "p50": 12.436, + "p95": 101.29, + "min": -22.262, + "max": 108.043 + }, + "baseline.failure.refused-disabled": { + "samples": [ + 8.442, + -77.994, + -5.175, + 76.722, + -7.1, + -66.218, + 4.013, + 102.311, + 23.022, + -63.568, + -0.475, + 73.688, + 29.953, + 24.863, + 10.291, + 73.233, + 8.488, + -19.87, + 9.838, + -16.445 + ], + "p50": 8.442, + "p95": 76.722, + "min": -77.994, + "max": 102.311 + }, + "baseline.failure.hanging-disabled": { + "samples": [ + 27.895, + 0.163, + -13.623, + 73.235, + -2.978, + -1.332, + -6.371, + 78.864, + 4.423, + 10.156, + -14.279, + 60.515, + 12.896, + 59.873, + 45.142, + 102.078, + 4.52, + 45.349, + 15.319, + -3.618 + ], + "p50": 10.156, + "p95": 78.864, + "min": -14.279, + "max": 102.078 + }, + "candidate.success.healthy-disabled": { + "samples": [ + 85.55, + 1.49, + 11.892, + 102.373, + 84.349, + 15.872, + 0.868, + -0.615, + 66.753, + 17.576, + 23.262, + 13.136, + -37.736, + 72.355, + 9.386, + 150.784, + 99.493, + 13.861, + 11.809, + 23.177 + ], + "p50": 15.872, + "p95": 102.373, + "min": -37.736, + "max": 150.784 + }, + "candidate.success.refused-disabled": { + "samples": [ + 17.112, + 8.111, + 15.365, + 46.091, + 9.098, + 4.731, + -12.931, + 3.717, + 2.091, + 22.549, + 14.181, + 38.698, + -100.788, + -9.721, + 27.139, + 6.109, + 11.977, + -32.834, + 17.392, + 61.244 + ], + "p50": 9.098, + "p95": 46.091, + "min": -100.788, + "max": 61.244 + }, + "candidate.success.hanging-disabled": { + "samples": [ + 29.864, + 22.747, + 103.979, + 40.81, + 36.676, + 27.704, + 101.066, + 33.975, + 11.32, + 40.225, + 86.913, + 72.587, + -42.253, + 18.329, + 99.912, + -4.074, + 20.162, + 26.406, + 110.871, + 69.91 + ], + "p50": 33.975, + "p95": 103.979, + "min": -42.253, + "max": 110.871 + }, + "candidate.failure.healthy-disabled": { + "samples": [ + -84.287, + -5.894, + -2.557, + -16.672, + -61.327, + -14.387, + -3.054, + 3.002, + -79.961, + 19.703, + 95.108, + -4.046, + 7.407, + 2.755, + 16.972, + -33.49, + -57.405, + 10.582, + 18.094, + -22.655 + ], + "p50": -4.046, + "p95": 19.703, + "min": -84.287, + "max": 95.108 + }, + "candidate.failure.refused-disabled": { + "samples": [ + 5.647, + -1.049, + -2.795, + -24.455, + 19.546, + -19.485, + -1.217, + -0.03, + 7.308, + 8.755, + 5.062, + -22.132, + 47.627, + 118.865, + 13.154, + -8.839, + 20.73, + 38.767, + -5.908, + -42.211 + ], + "p50": -0.03, + "p95": 47.627, + "min": -42.211, + "max": 118.865 + }, + "candidate.failure.hanging-disabled": { + "samples": [ + 13.323, + -5.065, + -81.87, + -21.403, + 14.052, + 3.572, + -85.274, + -1.05, + -3.172, + 10.638, + -77.262, + -11.873, + 19.342, + 69.641, + -64.739, + -11.791, + 7.69, + 2.511, + -47.171, + -38.087 + ], + "p50": -5.065, + "p95": 19.342, + "min": -85.274, + "max": 69.641 + }, + "candidate-baseline.success.disabled": { + "samples": [ + -12.024, + -18.687, + -4.283, + -102.573, + -9.102, + 0.509, + 4.26, + -65.275, + -22.72, + -7.645, + -26.744, + -78.28, + 15.013, + -18.828, + -12.129, + -105.253, + -3.503, + -3.577, + -8.411, + -98.186 + ], + "p50": -12.129, + "p95": 4.26, + "min": -105.253, + "max": 15.013 + }, + "candidate-baseline.success.healthy": { + "samples": [ + 57.837, + -14.805, + -16.217, + 86.162, + 68.269, + 7.895, + -23.874, + -12.905, + 32.336, + 11.044, + 13.094, + -21.109, + 85.897, + 30.331, + -8.148, + 131.593, + 76.234, + -7.544, + -14.859, + -5.045 + ], + "p50": 7.895, + "p95": 86.162, + "min": -23.874, + "max": 131.593 + }, + "candidate-baseline.success.refused": { + "samples": [ + -0.5, + -99.818, + -1.939, + -2.793, + -4.534, + -89.791, + -10.606, + -3.413, + -26.689, + -77.828, + -12.798, + -13.317, + -71.868, + -85.803, + -30.14, + -22.861, + -8.895, + -82.618, + 2.761, + -11.475 + ], + "p50": -13.317, + "p95": -0.5, + "min": -99.818, + "max": 2.761 + }, + "candidate-baseline.success.hanging": { + "samples": [ + 1.304, + -16.948, + 77.161, + -15.147, + 8.071, + 6.859, + 92.508, + 10.134, + -21.418, + 7.974, + 49.421, + -3.908, + -48.982, + -0.73, + 66.807, + -18.549, + -28.828, + 27.916, + 88.612, + -9.326 + ], + "p50": 1.304, + "p95": 88.612, + "min": -48.982, + "max": 92.508 + }, + "candidate-baseline.failure.disabled": { + "samples": [ + -1.458, + 4.079, + -3.25, + 92.196, + -39.401, + 12.574, + -6.879, + 77.905, + 11.015, + -3.281, + -21.812, + 63.713, + -6.132, + -21.96, + -8.012, + 89.533, + -6.639, + -0.124, + -8.785, + 13.893 + ], + "p50": -3.25, + "p95": 89.533, + "min": -39.401, + "max": 92.196 + }, + "candidate-baseline.failure.healthy": { + "samples": [ + -98.182, + -0.117, + -19.532, + -13.86, + -78.466, + -22.682, + -7.364, + -27.136, + -78.306, + 5.557, + 94.386, + -20.639, + -72.975, + -28.768, + 0.14, + -45.248, + -77.612, + -4.19, + 11.645, + -25.663 + ], + "p50": -22.682, + "p95": 11.645, + "min": -98.182, + "max": 94.386 + }, + "candidate-baseline.failure.refused": { + "samples": [ + -4.253, + 81.024, + -0.869, + -8.981, + -12.755, + 59.307, + -12.109, + -24.436, + -4.699, + 69.042, + -16.275, + -32.106, + 11.542, + 72.043, + -5.148, + 7.461, + 5.603, + 58.514, + -24.531, + -11.873 + ], + "p50": -4.699, + "p95": 72.043, + "min": -32.106, + "max": 81.024 + }, + "candidate-baseline.failure.hanging": { + "samples": [ + -16.031, + -1.149, + -71.497, + -2.442, + -22.371, + 17.478, + -85.782, + -2.009, + 3.42, + -2.799, + -84.795, + -8.675, + 0.313, + -12.191, + -117.893, + -24.336, + -3.469, + -42.962, + -71.275, + -20.576 + ], + "p50": -16.031, + "p95": 3.42, + "min": -117.893, + "max": 17.478 + } + }, + "pristine": [ + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 523.5014999999839, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 450.3583749999816, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 437.77699999997276, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 468.34370800000033, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 456.10866699999315, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 470.1848330000066, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 464.59833300000173, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 490.9200420000125, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "baseline", + "condition": "disabled", + "elapsedMs": 456.32795899998746, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "baseline", + "condition": "healthy", + "elapsedMs": 476.19216599999345, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + }, + { + "build": "candidate", + "condition": "disabled", + "elapsedMs": 457.88066699999035, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 0, + "openSocketsAfterExit": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextEventsVerified": 0, + "files": [], + "queue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + }, + "serverReceivedEventCounts": {} + }, + { + "build": "candidate", + "condition": "healthy", + "elapsedMs": 471.64891700001317, + "exitCode": 0, + "stdoutBytes": 3674, + "stderrBytes": 0, + "requests": 1, + "openSocketsAfterExit": 0, + "redirected": 1, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 1, + "projectContextEventsVerified": 2, + "files": [ + ".mex/telemetry-id", + ".mex/telemetry/outbox.db" + ], + "queue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + }, + "serverReceivedEventCounts": { + "cli.command_started:": 1, + "cli.command_completed:success": 1 + } + } + ], + "projectContext": { + "configuredAiTools": [ + "claude", + "codex" + ], + "existingScaffoldUuidMatched": true, + "privateSentinelsRejected": true, + "canonicalFixtureUnchanged": true, + "failureFixture": "ROUTER names one absent source file; check --json must return MISSING_PATH with exit1", + "canonicalFixtureSha256": "5313a6ac40febbd60f4d23c3630d54c40f97e188008843b4e7310556d768c3bf", + "verification": "Complete received and durable queued event envelopes were checked before projection. Candidate events must match the existing fixture UUID and exactly [claude,codex]; baseline events must omit both restored fields. Unrelated names, URLs, paths, content and email sentinels are rejected. Counts include baseline absence checks, not baseline metadata collection." + }, + "moduleProbe": { + "methodology": "Separately bundled current internal telemetry module; direct synchronous capture-call and awaited flush timings. Module/compiler startup excluded. Each repetition resets transport handles and keeps its isolated durable queue; first capture includes queue initialization. This does not replace built-CLI lifetime measurements. The project mode times getProjectTelemetryContext separately once per repetition and reuses its snapshot for both capture calls; this consent-gated helper skips project reads when disabled.", + "bundleSha256": "ae53d1c172484fb343f228f614f456766f0cf85a530005ef3a75a8f82ae963d7", + "conditions": { + "disabled": { + "captureStartedMs": { + "samples": [ + 0.002, + 0.001, + 0.001, + 0.001, + 0.003, + 0.002, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.002, + "min": 0.001, + "max": 0.003 + }, + "captureCompletedMs": { + "samples": [ + 0.002, + 0.002, + 0.002, + 0.002, + 0.002, + 0.002, + 0.002, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.002, + 0.002, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.002, + "min": 0.001, + "max": 0.002 + }, + "flushMs": { + "samples": [ + 0.001, + 0.001, + 0.001, + 0.054, + 0.006, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.006, + "min": 0.001, + "max": 0.054 + }, + "firstCaptureStartedMs": 0.1108749999999965, + "queue": { + "state": "absent", + "events": 0, + "bytes": 0 + }, + "requests": 0, + "redirected": 0, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 0, + "projectContextReadMs": { + "samples": [ + 0.001, + 0.001, + 0.001, + 0.001, + 0.002, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0, + 0.001, + 0.001 + ], + "p50": 0.001, + "p95": 0.001, + "min": 0, + "max": 0.002 + }, + "projectContextEventsVerified": 0, + "verifiedQueue": { + "state": "absent", + "events": 0, + "payloadBytes": 0, + "databaseBytes": 0, + "activeClaims": 0, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {} + } + }, + "healthy": { + "captureStartedMs": { + "samples": [ + 0.731, + 0.655, + 0.685, + 0.667, + 0.707, + 0.674, + 0.598, + 0.859, + 0.626, + 0.603, + 0.616, + 0.597, + 0.59, + 0.654, + 0.54, + 0.574, + 0.633, + 0.593, + 0.634, + 0.574 + ], + "p50": 0.626, + "p95": 0.731, + "min": 0.54, + "max": 0.859 + }, + "captureCompletedMs": { + "samples": [ + 0.49, + 0.463, + 0.45, + 0.435, + 0.457, + 0.5, + 0.434, + 0.436, + 0.441, + 0.414, + 0.46, + 0.448, + 0.412, + 0.446, + 0.405, + 0.532, + 0.444, + 0.414, + 0.435, + 0.399 + ], + "p50": 0.441, + "p95": 0.5, + "min": 0.399, + "max": 0.532 + }, + "flushMs": { + "samples": [ + 1.62, + 1.591, + 1.829, + 2.219, + 1.823, + 1.336, + 1.598, + 1.423, + 1.218, + 1.341, + 1.36, + 1.369, + 1.456, + 1.281, + 1.224, + 1.358, + 1.201, + 1.147, + 1.238, + 1.083 + ], + "p50": 1.358, + "p95": 1.829, + "min": 1.083, + "max": 2.219 + }, + "firstCaptureStartedMs": 3.2704169999999984, + "queue": { + "state": "available", + "events": 0, + "bytes": 0 + }, + "requests": 25, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 0, + "responseCompletions": 25, + "projectContextReadMs": { + "samples": [ + 0.204, + 0.221, + 0.169, + 0.246, + 0.172, + 0.197, + 0.211, + 0.146, + 0.163, + 0.168, + 0.168, + 0.145, + 0.153, + 0.172, + 0.142, + 0.15, + 0.167, + 0.139, + 0.149, + 0.142 + ], + "p50": 0.167, + "p95": 0.221, + "min": 0.139, + "max": 0.246 + }, + "projectContextEventsVerified": 50, + "verifiedQueue": { + "state": "available", + "events": 0, + "payloadBytes": 0, + "activeClaims": 0, + "databaseBytes": 12288, + "eventCounts": {}, + "receivedOrQueuedEventCounts": {}, + "projectContextEventsVerified": 0 + } + }, + "refused": { + "captureStartedMs": { + "samples": [ + 0.795, + 0.679, + 0.688, + 0.709, + 0.699, + 0.662, + 0.62, + 0.603, + 0.648, + 0.605, + 0.649, + 0.648, + 0.707, + 0.618, + 0.782, + 0.584, + 0.698, + 0.666, + 0.727, + 0.725 + ], + "p50": 0.666, + "p95": 0.782, + "min": 0.584, + "max": 0.795 + }, + "captureCompletedMs": { + "samples": [ + 0.53, + 0.463, + 0.444, + 0.445, + 0.621, + 0.475, + 0.421, + 0.731, + 0.454, + 0.436, + 0.48, + 0.473, + 0.461, + 0.495, + 0.47, + 0.439, + 0.49, + 0.46, + 0.482, + 0.514 + ], + "p50": 0.47, + "p95": 0.621, + "min": 0.421, + "max": 0.731 + }, + "flushMs": { + "samples": [ + 1.796, + 1.638, + 1.944, + 2.201, + 2.248, + 1.378, + 1.302, + 1.76, + 1.57, + 1.847, + 1.727, + 1.83, + 1.539, + 2.054, + 1.484, + 1.556, + 1.745, + 1.516, + 1.607, + 1.691 + ], + "p50": 1.691, + "p95": 2.201, + "min": 1.302, + "max": 2.248 + }, + "firstCaptureStartedMs": 3.2354579999999977, + "queue": { + "state": "available", + "events": 50, + "bytes": 27100 + }, + "requests": 0, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 25, + "responseCompletions": 0, + "projectContextReadMs": { + "samples": [ + 0.21, + 0.186, + 0.158, + 0.234, + 0.166, + 0.195, + 0.141, + 0.138, + 0.157, + 0.153, + 0.16, + 0.145, + 0.183, + 0.142, + 0.173, + 0.14, + 0.157, + 0.143, + 0.136, + 0.18 + ], + "p50": 0.157, + "p95": 0.21, + "min": 0.136, + "max": 0.234 + }, + "projectContextEventsVerified": 0, + "verifiedQueue": { + "state": "available", + "events": 50, + "payloadBytes": 27100, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + } + }, + "hanging": { + "captureStartedMs": { + "samples": [ + 1.153, + 1.168, + 1.246, + 1.179, + 1.071, + 1.188, + 0.925, + 1.599, + 2.466, + 2.353, + 1.89, + 2.64, + 1.345, + 1.639, + 1.117, + 1.104, + 1.043, + 1, + 0.964, + 1.058 + ], + "p50": 1.168, + "p95": 2.466, + "min": 0.925, + "max": 2.64 + }, + "captureCompletedMs": { + "samples": [ + 0.663, + 0.699, + 0.735, + 0.726, + 0.662, + 0.702, + 0.616, + 0.804, + 1.347, + 1.35, + 1.163, + 1.376, + 0.978, + 1.264, + 0.561, + 0.701, + 0.546, + 0.562, + 0.575, + 0.601 + ], + "p50": 0.701, + "p95": 1.35, + "min": 0.546, + "max": 1.376 + }, + "flushMs": { + "samples": [ + 27.757, + 26.781, + 27.542, + 27.186, + 27.287, + 27.441, + 25.965, + 28.886, + 28.661, + 27.06, + 29.13, + 26.35, + 27.599, + 27.784, + 27.6, + 28.308, + 27.451, + 26.519, + 26.182, + 27.686 + ], + "p50": 27.451, + "p95": 28.886, + "min": 25.965, + "max": 29.13 + }, + "firstCaptureStartedMs": 3.350625000000001, + "queue": { + "state": "available", + "events": 50, + "bytes": 27100 + }, + "requests": 25, + "redirected": 25, + "blocked": 0, + "blockedKind": null, + "requestErrors": 25, + "responseCompletions": 0, + "projectContextReadMs": { + "samples": [ + 0.314, + 0.401, + 0.373, + 0.418, + 0.388, + 0.266, + 0.347, + 0.702, + 0.539, + 0.577, + 0.659, + 0.486, + 0.578, + 0.487, + 0.481, + 0.326, + 0.28, + 0.277, + 0.258, + 0.302 + ], + "p50": 0.388, + "p95": 0.659, + "min": 0.258, + "max": 0.702 + }, + "projectContextEventsVerified": 560, + "verifiedQueue": { + "state": "available", + "events": 50, + "payloadBytes": 27100, + "activeClaims": 0, + "databaseBytes": 49152, + "eventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "receivedOrQueuedEventCounts": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "projectContextEventsVerified": 50 + } + } + } + }, + "limitations": [ + "Single local host; paired differences include scheduler and module-loading noise.", + "Server receipt is not client acknowledgement: hanging ingestion receives bodies but never acknowledges them. Client HTTP response completions and durable retained events are reported separately; receipt/retention accounting deduplicates UUIDs in memory and retains no identifiers.", + "Local HTTP ingestion does not measure production TLS or Internet latency.", + "Candidate offline queues accumulate across the repeated invocations; the queue remains bounded and is inspected only after timed runs.", + "First-command runs are bounded observations, not a statistically stable p95.", + "This comparison restores metadata against commit 9cbfab8. The earlier telemetry report used a different baseline and empty-project failure fixture; do not treat their absolute timings as a matched before/after comparison.", + "The initialized project deliberately contains a missing path and an orphaned scaffold document so check --json exercises a real failed validation. The baseline and candidate use the same bytes and both return exit1.", + "This scenario accumulates up to 50 queued events. Maximum-cap queue performance, arbitrary ancestor depths/config sizes, and production network delivery rates are not measured here.", + "Both builds use the same preload instrumentation. Embedded Yoga WASM data-URL loading is allowed because it performs no network I/O. The metadata module probe measures the actual consent-gated helper once, then reuses its snapshot for both captures; compiler/module startup is excluded from that probe only." + ], + "provenance": { + "baselineCommit": "9cbfab8f694189e79080b4887b42d8e0aa40ea50", + "candidate": "Working-tree project telemetry metadata implementation on codex/0.8.1-telemetry; exact built CLI SHA-256 recorded above", + "rawReportSha256": "06a2ea70a458fc69701bb979c289c146c31bc1f5b17d2b1ad2af1e1faa76d309", + "harnessSha256": "91a4cc6982b3099741314dbe79fd7f30e7615e557f012ab5f229edb89e6ed266", + "rawReportBytes": 413010, + "baselinePackageSha256": "6799e4bca1c95d89cfa16389194f742a331964ba5a0a2c00accb89ce87867bf5", + "baselineLockSha256": "019f993bc923ba6568f26863d2576eab54760b58164048c47f897b601a6d89f0", + "recording": "All raw timing and paired-difference samples retained here; repeated received bodies replaced by per-run closed event counts. No identifiers, tokens or absolute local paths are retained. An interrupted run before the final root-attribution fix was discarded." + }, + "findings": { + "candidateProcessMs": { + "success": { + "disabled": { + "p50": 396.824, + "p95": 483.518 + }, + "healthy": { + "p50": 454.433, + "p95": 530.903 + }, + "refused": { + "p50": 403.237, + "p95": 489.627 + }, + "hanging": { + "p50": 451.571, + "p95": 525.614 + } + }, + "failure": { + "disabled": { + "p50": 564.943, + "p95": 630.521 + }, + "healthy": { + "p50": 549.491, + "p95": 625.81 + }, + "refused": { + "p50": 570.042, + "p95": 634.786 + }, + "hanging": { + "p50": 556.562, + "p95": 618.729 + } + } + }, + "candidateEnabledMinusDisabledMs": { + "success": { + "healthy": { + "p50": 15.872, + "p95": 102.373 + }, + "refused": { + "p50": 9.098, + "p95": 46.091 + }, + "hanging": { + "p50": 33.975, + "p95": 103.979 + } + }, + "failure": { + "healthy": { + "p50": -4.046, + "p95": 19.703 + }, + "refused": { + "p50": -0.03, + "p95": 47.627 + }, + "hanging": { + "p50": -5.065, + "p95": 19.342 + } + } + }, + "candidateMinusBaselineMs": { + "success": { + "disabled": { + "p50": -12.129, + "p95": 4.26 + }, + "healthy": { + "p50": 7.895, + "p95": 86.162 + }, + "refused": { + "p50": -13.317, + "p95": -0.5 + }, + "hanging": { + "p50": 1.304, + "p95": 88.612 + } + }, + "failure": { + "disabled": { + "p50": -3.25, + "p95": 89.533 + }, + "healthy": { + "p50": -22.682, + "p95": 11.645 + }, + "refused": { + "p50": -4.699, + "p95": 72.043 + }, + "hanging": { + "p50": -16.031, + "p95": 3.42 + } + } + }, + "projectContextReadMs": { + "disabled": { + "p50": 0.001, + "p95": 0.001 + }, + "healthy": { + "p50": 0.167, + "p95": 0.221 + }, + "refused": { + "p50": 0.157, + "p95": 0.21 + }, + "hanging": { + "p50": 0.388, + "p95": 0.659 + } + }, + "captureCallP95MaxMs": 2.466, + "firstCaptureMaxMs": 3.350625000000001, + "flushObservedMaxMs": 29.13, + "healthyEvidence": { + "success": { + "uniqueReceivedIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + }, + "retainedQueue": {}, + "uniqueReceivedOrQueued": { + "cli.command_started:": 25, + "cli.command_completed:success": 25 + } + }, + "failure": { + "uniqueReceivedIncludingWarmups": { + "cli.command_started:": 25, + "cli.command_completed:failure": 24 + }, + "retainedQueue": { + "cli.command_completed:failure": 1 + }, + "uniqueReceivedOrQueued": { + "cli.command_started:": 25, + "cli.command_completed:failure": 25 + } + } + }, + "offlineEvidence": { + "success.refused": { + "events": 50, + "payloadBytes": 27100, + "databaseBytes": 49152, + "activeClaims": 0, + "projectContextEventsVerified": 50 + }, + "success.hanging": { + "events": 50, + "payloadBytes": 27100, + "databaseBytes": 49152, + "activeClaims": 0, + "projectContextEventsVerified": 50 + }, + "failure.refused": { + "events": 50, + "payloadBytes": 26975, + "databaseBytes": 49152, + "activeClaims": 0, + "projectContextEventsVerified": 50 + }, + "failure.hanging": { + "events": 50, + "payloadBytes": 26975, + "databaseBytes": 49152, + "activeClaims": 0, + "projectContextEventsVerified": 50 + } + }, + "isolationAndCleanup": "All disabled runs made zero telemetry requests. Every measured child exited naturally with its expected output and exit code; no server sockets remained afterward. Disabled pristine homes stayed empty. The canonical project fixture was byte-identical after all commands and probes.", + "interpretation": "The consent-gated project lookup remained below 0.7 ms at p95 in this local fixture, with capture-call p95 below 5 ms and observed flush maximum below 50 ms. Whole-process paired tails were noisy, including disabled baseline/candidate runs; the healthy success median increased 7.895 ms and its paired p95 increased 86.162 ms. These data do not establish a portable no-overhead guarantee or a command speedup." + } +} diff --git a/docs/design/telemetry-v2.md b/docs/design/telemetry-v2.md new file mode 100644 index 00000000..987b734f --- /dev/null +++ b/docs/design/telemetry-v2.md @@ -0,0 +1,259 @@ +# MEX 0.8.1 telemetry + +Branch: `codex/0.8.1-telemetry`, based on release commit `d64f171`. +This change targets `codex/0.8.1`; it does not authorize merging to main. +Draft PR: [#188](https://github.com/mex-memory/mex/pull/188). +The initial implementation and retained measurements are from `9cbfab8`. +The subsequently approved project-context additions are locally verified in +their separate addendum below; new full and platform CI remains required. + +## Product questions and measurement definitions + +The shared random installation UUID is explicitly approved for CLI and Hub. +The user subsequently approved restoring the existing scaffold UUID and adding +configured AI-tool names. An installation remains the repeat-use unit; multiple +installations using one scaffold provide a shared-project signal. This cannot +establish people, organizations, team membership, or team size. Agent-generated +command activity is included and cannot be reliably separated from human +invocations, and configured tools do not identify the invoking agent. + +| Question | Definition | +| --- | --- | +| Which features get used? | Unique installations and completed invocations by `command`, or explicit Hub `action`; keep CLI/Hub source and stage visible. Report raw volume alongside unique installations so agent loops do not dominate adoption. | +| Where do operations fail? | Failures / completions for the same command/action/job kind and stage. Report job results separately from successful job-start requests. Invalid input rejected before dispatch is outside this denominator. | +| Are new installations activated? | First observed meaningful completion or Hub page visit, followed by a successful knowledge/graph use or applied contribution/Relay. “First observed” is not guaranteed first install because delivery is opt-out and best effort. | +| Do installations return? | UTC day-1/day-7 retention after first meaningful activity: another qualifying activity on that exact later day. Offer a separate rolling-week measure rather than calling both D7 retention. | +| Is use habitual? | DAU/WAU and DAU/MAU of qualifying installation activity; also active days per installation per week. Use completed calendar windows and original event timestamps. | +| Is the Hub useful? | Returning installations with page navigation or explicit actions; views by category; successful apply and terminal job outcomes. A session start, idle timer, polling request, or SSE reconnect is not active use. | +| Does a project appear to be shared? | During the last 28 completed UTC days, count distinct installation IDs with qualifying activity per nonempty `scaffold_id`. Two or more is a shared-project estimate. For an adoption percentage, divide qualifying scaffold IDs with at least two installations by all observed qualifying scaffold IDs; report missing-context coverage separately. | +| Which AI integrations are configured? | Break down observed project/installation activity by membership in `configured_ai_tools`. Label this configured-tool adoption, not actual agent usage. Selections can overlap, so percentages need not sum to 100%; state whether the denominator is distinct installations or distinct scaffold IDs. | + +Qualifying CLI engagement is a completed product operation, including a failed +attempt. Exclude `commands`, `completion`, `feedback`, `log`, `heartbeat`, +`doctor`, `check`, `watch`, and launcher-only `mex`/`tui` from the primary +engagement view; keep them in diagnostic feature-use reports. This is an +analysis filter, not a claim about whether an action was automated. CLI +`preview` counts intent but not an applied change. Hub `replayed: true` applies +count as invocations, not additional mutations. CLI completion does not expose +replay detail, so CLI invocations must not be presented as unique mutations. + +Shared-project estimates can overcount one person's several machines or copied +scaffolds that retain an ID, and undercount users sharing one installation. +Reset installation IDs also change the count. A scaffold UUID has no embedded +name, but anyone with access to a project's config can associate it with that +project. Do not present the signal as verified team accounts or anonymized +project identities. Missing context is unknown, not a new or single-user +project. + +`configured_ai_tools` is the saved project selection: `claude`, `codex`, +`copilot`, `cursor`, `opencode`, and/or `windsurf`. Several selections are valid; +the payload uses a sorted, unique array of at most six names. No running +processes, parent shells, editor sessions, or account data are inspected. The +selection can be stale or differ from the agent actually invoking MEX. + +Do not join voluntary research contact details to installation IDs. The CLI +and Hub open the same hosted form without identifiers in URL parameters. +The form fields and follow-up consent are managed outside this repository. + +## Implementation + +- `src/telemetry/schema.ts` defines the complete vocabulary and reconstructs + allowlisted payloads. All six events accept optional `scaffold_id` and + `configured_ai_tools`; a scaffold ID must be exactly a UUIDv4, and tool values + come from the six-name catalog. Stored events are revalidated before delivery. + These are additive fields in unreleased schema v2; older queued v2 events + without them remain valid. +- `src/telemetry/project-context.ts` reads bounded existing config bytes through + a read-only, file-identity-checked snapshot. It never initializes, repairs, or + mints project identity. Missing, malformed, unreadable, or unsafe config omits + metadata; invalid IDs and unknown tool names are excluded. Ordinary CLI + operations reuse one snapshot, while setup/init reread at completion for newly + saved configuration. The Hub binds a snapshot to its project on the first + enabled event; subsequent config changes appear after Hub restart. No project + names, Git remotes, paths, content, or contact details enter that projection. + Graph maintenance/status metadata follows the exact selected root, honoring + local `--root` before the parent option; Hub metadata also uses its exact root. + A missing nested scaffold never falls back to an unrelated ancestor identity. + Other CLI operations retain nearest-Git-root discovery. +- `src/cli-telemetry.ts` uses registered Commander ancestry, derives a closed + stage value, and completes each action once. Action errors retain exit codes + and output while allowing bounded telemetry cleanup. TUI/watch launcher + completion describes startup, not eventual session termination. +- `src/hub/telemetry.ts` projects validated server actions. A strict, authenticated + page-category POST is capped at 128 bytes and 60 events/minute. Browser requests + have one in-flight operation and a two-second timeout. Query/hash changes, + polling, normal reads, fixture APIs, and repeated renders stay silent. +- Owned jobs emit a terminal outcome once after durable terminal-state storage; + reading or reconciling historical jobs does not create another completion. +- The per-user SQLite outbox uses zero busy wait, capped rows/bytes/age/file size, + rollback journals, and expiring claims. No graph database is opened or rebuilt. + The existing lazy SQLite adapter avoids loading native SQLite for opt-outs. +- Node HTTP sends fixed-origin batches with no SDK retries or detached process. + CLI flush has a default 25 ms grace, capped at 50 ms for internal callers; + Hub batches every 15 seconds with a two-second request timeout. Cancellation + destroys sockets and cancels the dedicated DNS resolver. +- Explicit opt-out persists before queue cleanup and reports a busy/unavailable + purge honestly. Its dedicated marker survives unrelated stale preference writes; + only explicit enable removes it. Inspection never creates identities, repairs queues, or sends. + +The complete privacy and delivery contract is [TELEMETRY.md](../../TELEMETRY.md). +Public package exports, emitted root declaration bytes, graph/Wiki protocols, +and repository artifact schemas are unchanged. `posthog-node` is removed from +production dependencies. The published package now includes `TELEMETRY.md`. The transport follows the documented +[PostHog capture API](https://posthog.com/docs/api/capture). + +## Performance verification + +The methodology below applies to future comparison runs. The retained results +are historical evidence for `9cbfab8`, before optional project context. The new +reader and added payload fields have separate measurements recorded in the +validation addendum. + +The standalone benchmark runs actual built CLI entry points from process spawn +through natural close. It pairs a preserved release build with this candidate, +interleaves conditions, and compares telemetry off, healthy loopback delivery, +connection refusal, and a server that accepts requests but never responds. +Success (`mex commands`) and failure (`mex check --json` outside a project) are +both covered, along with pristine-home initialization. Repeated invocations +verify queued completion accounting, output/exit compatibility, bounded request +cleanup, and offline retention. + +The harness redirects only the fixed telemetry ingestion origin to loopback +using a test-only Node preload, blocks other egress, and leaves the production +entry point unchanged. It does not measure internet/TLS latency; a separate +transport deadline test covers cancellation. These local measurements are +characterization, not a new portable release budget. + +Run after preserving a baseline build and completing other heavy checks: + +```sh +npm run benchmark:telemetry:test +npm run benchmark:telemetry -- --baseline /absolute/baseline/dist/cli.js --output test-results/telemetry.json +``` + +### Historical measured result: `9cbfab8` + +[Retained evidence](telemetry-performance-results.json) includes raw samples and +paired differences. On the local Apple M4 / Node 22.17.1 host, each of 16 CLI +groups completed five warmups and 20 measured invocations. Times below are +paired enabled-minus-disabled differences, in milliseconds. + +| Command result | Healthy p50 / p95 | Refused p50 / p95 | Hanging p50 / p95 | +| --- | --- | --- | --- | +| Success | 3.084 / 17.811 | 4.408 / 20.586 | 27.294 / 48.884 | +| Failure | 7.806 / 18.022 | 6.377 / 24.638 | 22.797 / 31.600 | + +The short successful command's absolute median was 357.033 ms disabled, +360.396 ms with healthy local ingestion, and 386.629 ms with hanging ingestion. +The older implementation took 1029.530 ms with hanging ingestion; the paired +median improvement was 644.888 ms. The new failure path adds outcome capture +where the old process exited without delivering an event. + +Capture-call p95 was at most 2.410 ms, with first queue initialization at most +3.210 ms. The longest observed flush was 29.114 ms, including local cleanup and +scheduling around the 25 ms network grace. These observed times are not hard +wall-clock guarantees on arbitrary disks, hosts, or schedulers. + +Every tested healthy candidate command accounted for all 25 starts and 25 +completions across warmups and samples, with an empty queue afterward. Refused +and hanging conditions retained all 50 events per command in a 45,056-byte +store, with no active delivery claim left. All processes exited naturally with +expected output/status, no sockets remained, and disabled pristine homes stayed +empty. Receipt and acknowledgement are distinguished; UUID set union prevents +received-but-still-queued events from being counted twice. + +Internet/TLS latency and production delivery rates are not measured by the +loopback benchmark. Short-only usage can defer events repeatedly until enough +runtime is available for a send or a Hub session drains the queue. The local +measurements cover backlogs up to 50 events; maximum-cap queue timing is not +claimed. The initial implementation validation record follows below. + +## Historical Verify Checklist: `9cbfab8` + +These results belong to the original telemetry implementation, before the +scaffold/tool follow-up. They remain retained evidence, not a pass for the +current working tree. + +1. **Public surface/declarations — pass.** `src/index.ts` and root emitted declaration bytes are unchanged; telemetry remains internal. +2. **Read and write safety — pass.** Pure reads skip capture; page input is authenticated and bounded; queued data is revalidated; explicit opt-out owns preference/cleanup. +3. **Deterministic bounds — pass.** Event vocabulary, request body/rate, queue rows/bytes/age/file size, claim life, requests and network grace are bounded. Tests include malformed data, unsafe paths, large SQLite pages, concurrent writers, active opt-out and real stalled DNS. +4. **Tests/typecheck — pass.** Final `npm test -- --maxWorkers=2`: 222 files, 3,500 passed, one skipped (657.24 seconds), with no concurrent build. Hub web: 434 passed. Playwright: two passed (Home screenshot/accessibility and production-Hub integration). Workspace typecheck and standalone benchmark harness (four tests) pass. The actual CLI benchmark passes. The first full run exposed two old-API architecture assertions and one aggregate containment timeout; the guard now covers the actual capture boundary, and the five containment fixtures run independently with their original assertions/timeouts. +5. **Packaging/evaluator — pass.** Full build and fresh packed-install/Project Hub/official-skills smoke pass. The packaged CLI hash matches the retained benchmark candidate. No evaluator or graph protocol change requires a new evaluator run. +6. **Diff and scope — pass.** `git diff --check` passes. Changes are limited to telemetry, feedback, supporting tests/CI and documentation. Generated indexes, checkout local state and dist remain unstaged; the real graph database hash is unchanged. +7. **Graph/Wiki protocols — pass.** Existing command, application-adapter, golden protocol, immutable-read and Hub integration suites pass. No graph/Wiki protocol, error, ordering, cursor, or maintenance contract changed. macOS/Windows CI and the pinned release-performance gate remain required before release. + +## Project-context validation addendum + +The follow-up adds only optional existing scaffold UUID/configured-tool context. +Its baseline is the initial telemetry revision `9cbfab8`, whose Node 22/24, +macOS/Windows, browser and release-performance CI all passed in run +`34361480149`. Those results are historical; the follow-up has separate local +checks and requires new CI before merge. + +[Retained project-context measurements](telemetry-project-context-performance.json) +compare the preserved `9cbfab8` build against candidate CLI SHA-256 +`99ebd1b40c434de47bcfdbe5db74af43a5be27afa84c96d0bc401cbdb52e067b`. +The Apple M4 / Node 22.17.1 run used five warmups and 20 samples for each of 16 +groups, plus pristine-home and module probes. Pass `--project-context` to the +benchmark command above: this creates an isolated existing scaffold/tool +configuration and uses a deliberate missing source path for the failed check. + +The enabled metadata helper measured p95 **0.221 ms** with healthy ingestion, +**0.210 ms** with refused connections and **0.659 ms** with hanging ingestion. +Capture-call p95 was at most **2.466 ms**, first capture at most **3.351 ms**, +and the longest observed flush was **29.130 ms**. + +Whole-process timing was noisy, so it does not establish zero overhead or a +speedup. These are paired candidate-minus-`9cbfab8` differences in milliseconds: + +| Command result | Disabled p50 / p95 | Healthy p50 / p95 | Refused p50 / p95 | Hanging p50 / p95 | +| --- | --- | --- | --- | --- | +| Success | -12.129 / 4.260 | 7.895 / 86.162 | -13.317 / -0.500 | 1.304 / 88.612 | +| Failure | -3.250 / 89.533 | -22.682 / 11.645 | -4.699 / 72.043 | -16.031 / 3.420 | + +The disabled comparison itself includes a roughly 90 ms p95 difference, where +project config is not read. Retain this noise rather than treating positive +tails as measured metadata cost or negative medians as an optimization. The +module probe isolates the small metadata cost; the full CLI run verifies real +process cleanup, outputs and delivery under the tested conditions. These are +local observations, not a portable latency guarantee. + +Every received and queued event passed exact existing UUID/tool checks and +private-field rejection. All processes exited naturally, sockets and delivery +claims were cleared, disabled pristine homes stayed empty, and canonical +fixture bytes were unchanged. Healthy success accounted for all 50 events; +healthy failure received 49 and retained the final completion locally, for a +50-event union. Refused/hanging queues retained 50 events in 49,152 bytes. This +also verifies the documented best-effort delivery boundary. All timing samples +are retained without event/installation/scaffold IDs or private fixture values. + +1. **Public surface/declarations — pass.** `src/index.ts` is unchanged, and the final build's root declaration bytes match the preserved `9cbfab8` build. The new reader and metadata types remain internal. +2. **Read and write safety — pass.** Metadata reads never mint identity or modify project state. Opt-outs skip discovery; unavailable or unsafe config omits context. Tests cover nested projects, exact Graph/Hub roots, parent/local Graph option precedence, read-only inspection, setup completion refresh and independent Hub snapshots. +3. **Deterministic bounds — pass.** Discovery is capped at 64 ancestors and 64 KiB of config. Only an existing UUIDv4 and at most six known, sorted, unique tool names survive projection. File identity, UTF-8, schema, symlink/junction and hardlink checks remain enforced. Queue and transport bounds are unchanged. +4. **Tests/typecheck — pass.** This follow-up passed 353 distinct tests across 15 files covering schema, reader, capture, CLI, Hub wiring/actions/jobs, delivery, capabilities, feedback and architecture. The final root-attribution correction was rechecked with 142 tests across six files, followed by workspace typecheck. Seven standalone benchmark-harness tests pass. The previous full regression, web and browser counts above belong to `9cbfab8`; no new full-suite result is claimed here. +5. **Packaging/evaluator — pass for the changed scope.** Full build passes, and the final built CLI is exercised by the dedicated latency harness. Package layout, dependencies and frontend assets are unchanged by this follow-up; the initial packed-install smoke remains historical evidence. No evaluator or Graph protocol change requires another evaluator run. +6. **Diff and scope — pass.** `git diff --check` passes, including the retained benchmark report. Source, tests, CI and docs are the intended changes; generated indexes, local state and dist remain excluded. The real graph database still matches its pre-change SHA-256 `0b6dd1f8e161f19e4a15111f125a83d2245f01c01e791f21e38f2c8a4a61af41`. +7. **Graph/Wiki protocols — pass for the changed scope.** No Graph/Wiki payload, error, ordering, cursor or maintenance implementation changed. The focused CLI/capability/immutable-read regressions pass; the attribution correction changes only which safe telemetry context is selected. Fresh full and platform CI remains required before merge. + +## Windows DNS test correction + +At `e725551`, CI run `34375530581` passed Node 22/24, macOS portability, +browser and release-performance checks. Windows passed 522 tests and skipped +three; one real DNS cancellation test measured 174.8385 ms against its 150 ms +elapsed limit. The new scaffold/tool tests passed on Windows. + +That test used a 10 ms sleep to assume dispatch had reached its loopback DNS +server. It now waits for the first observed UDP query and confirms it is still +pending before timing `flush`. This removes setup ambiguity without changing +the 25 ms production grace, 150 ms elapsed assertion, real `ECANCELLED` result, +stopped-query assertion or retained-event check. No production code changed. +The original log does not isolate whether cold setup, scheduling or local +cleanup caused its elapsed overrun; a fresh Windows run must verify the fix. + +1. **Public surface/declarations — pass.** Test/documentation-only correction; public source and built declarations are unchanged. +2. **Read/write safety — pass.** Real DNS remains pinned to a loopback blackhole. Cancellation and retained queue assertions are preserved. +3. **Bounds — pass.** Existing runtime and elapsed bounds are unchanged; waiting for the first query has a one-second setup guard. +4. **Tests/typecheck — pass locally.** All 45 delivery tests and workspace typecheck pass. Windows verification is pending the new CI run. +5. **Packaging/evaluator — not affected.** No runtime, packaging, asset or evaluator changes; the retained built-CLI measurements remain applicable. +6. **Diff/scope — pass.** `git diff --check` passes. Only the delivery test, this validation record and the telemetry test guidance changed. +7. **Graph/Wiki protocols — not affected.** No Graph/Wiki code, database, protocol, error or maintenance change. diff --git a/flake.nix b/flake.nix index c9031cb1..01249245 100644 --- a/flake.nix +++ b/flake.nix @@ -18,7 +18,7 @@ { default = pkgs.buildNpmPackage { pname = "mex"; - version = "0.8.0"; + version = (builtins.fromJSON (builtins.readFile ./package.json)).version; src = ./.; diff --git a/package-lock.json b/package-lock.json index de4ff63a..5d896bd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mex-agent", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mex-agent", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "workspaces": [ "packages/*" @@ -19,7 +19,6 @@ "glob": "^13.0.6", "hono": "^4.13.3", "ink": "^7.0.3", - "posthog-node": "^5.21.2", "react": "^19.2.6", "remark-frontmatter": "^5.0.0", "remark-parse": "^11.0.0", @@ -1216,15 +1215,6 @@ "node": ">=20" } }, - "node_modules/@posthog/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.10.0.tgz", - "integrity": "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -5456,18 +5446,6 @@ } } }, - "node_modules/posthog-node": { - "version": "5.21.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.2.tgz", - "integrity": "sha512-Jehlu0KguL1LLyUczCt86OtA5INmeStK3zcgbv1BSyMcNxs0HP3GQogBrYhwhqHsk6JopiFFVpJyZEoXOUMhGw==", - "license": "MIT", - "dependencies": { - "@posthog/core": "1.10.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index 6c30a3f3..8e1ddc5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mex-agent", - "version": "0.8.0", + "version": "0.8.1", "description": "Persistent project memory and deterministic local code graphs for AI coding agents", "type": "module", "main": "./dist/index.js", @@ -24,6 +24,7 @@ "templates", "LICENSE", "README.md", + "TELEMETRY.md", "COMPATIBILITY.md" ], "keywords": [ @@ -72,7 +73,9 @@ "benchmark:graph-status": "npm run build && node scripts/benchmark-graph-status.mjs", "benchmark:release": "npm run build && node scripts/release-benchmark/enforce.mjs --output test-results/release-benchmark/report.json", "typecheck": "npm run typecheck --workspace @mex/hub-contracts && tsc --noEmit && npm run typecheck --workspace @mex/hub-web", - "prepare": "npm run build" + "prepare": "npm run build", + "benchmark:telemetry": "npm run build && node scripts/benchmark-telemetry.mjs", + "benchmark:telemetry:test": "node --test scripts/benchmark-telemetry.test.js" }, "dependencies": { "@hono/node-server": "^1.19.17", @@ -82,7 +85,6 @@ "glob": "^13.0.6", "hono": "^4.13.3", "ink": "^7.0.3", - "posthog-node": "^5.21.2", "react": "^19.2.6", "remark-frontmatter": "^5.0.0", "remark-parse": "^11.0.0", diff --git a/packages/hub-contracts/src/inbox-knowledge.test.ts b/packages/hub-contracts/src/inbox-knowledge.test.ts new file mode 100644 index 00000000..0544ceda --- /dev/null +++ b/packages/hub-contracts/src/inbox-knowledge.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { InboxDraftInputSchema, InboxKnowledgeKindSchema, InboxProposalSummarySchema } from "./index.js"; + +const id = "mx_01000000000000000000000001"; +const draft = { + change: { kind: "knowledge.create", entityKind: "pattern", title: "Preserve reviewed evidence", body: "Keep the accepted baseline until review.", status: "in_flight" }, + rationale: "An automatic refresh must not erase drift.", evidence: [], targetRevisions: [], +}; + +describe("private Hub knowledge Inbox contracts", () => { + it.each(InboxKnowledgeKindSchema.options)("accepts one %s create with unchanged canonical text", (entityKind) => { + const input = { ...draft, change: { ...draft.change, entityKind, summary: "", body: "First line\n\tSecond line" } }; + expect(InboxDraftInputSchema.parse(input)).toEqual(input); + }); + + it("retains the legacy Spec discriminators and rejects crossed kinds or knowledge hierarchy edits", () => { + expect(InboxDraftInputSchema.safeParse({ ...draft, change: { ...draft.change, kind: "spec.create", entityKind: "spec" } }).success).toBe(true); + for (const change of [ + { ...draft.change, entityKind: "spec" }, + { ...draft.change, kind: "spec.create" }, + { ...draft.change, relation: { type: "related_to", target: { id, kind: "pattern" } } }, + ]) expect(InboxDraftInputSchema.safeParse({ ...draft, change }).success).toBe(false); + }); + + it("requires exact target coverage and keeps explicit summary clearing distinct from an empty update", () => { + const input = { ...draft, + change: { kind: "knowledge.update", target: { id, kind: "architecture" }, patch: { summary: "" } }, + targetRevisions: [{ target: { kind: "entity", id }, revision: "a".repeat(64), semanticRevision: 3 }], + }; + expect(InboxDraftInputSchema.parse(input)).toEqual(input); + expect(InboxDraftInputSchema.safeParse({ ...input, targetRevisions: [] }).success).toBe(false); + expect(InboxDraftInputSchema.safeParse({ ...input, targetRevisions: [...input.targetRevisions, ...input.targetRevisions] }).success).toBe(false); + expect(InboxDraftInputSchema.safeParse({ ...input, change: { ...input.change, patch: {} } }).success).toBe(false); + expect(InboxDraftInputSchema.safeParse({ ...input, change: { ...input.change, patch: { status: "promoted" } } }).success).toBe(false); + }); + + it("allows knowledge summaries without changing proposal identity or review authority rules", () => { + const proposalId = "proposal_01000000000000000000001720"; + const summary = { + schemaVersion: 1, ref: { id: proposalId, kind: "proposal" }, sourcePath: `.mex/inbox/${proposalId}.md`, + revision: "b".repeat(64), state: "pending", author: { kind: "unknown" }, + changeKind: "knowledge.update", entityKind: "guide", title: "Setup guide", rationaleExcerpt: "Clarify installation.", + }; + expect(InboxProposalSummarySchema.safeParse(summary).success).toBe(true); + expect(InboxProposalSummarySchema.safeParse({ ...summary, state: "approved" }).success).toBe(false); + }); +}); diff --git a/packages/hub-contracts/src/index.test.ts b/packages/hub-contracts/src/index.test.ts index 8f51687c..1c7ffa21 100644 --- a/packages/hub-contracts/src/index.test.ts +++ b/packages/hub-contracts/src/index.test.ts @@ -55,6 +55,8 @@ import { WikiEntityIdSchema, WikiEntityListRequestSchema, WikiEntityListResponseSchema, + WikiGraphResponseSchema, + WikiGroundedCodeResponseSchema, WikiRelationsRequestSchema, } from "./index.js"; @@ -537,8 +539,8 @@ describe("Hub API contracts", () => { expectedRevisions: [memberTarget, draftTarget], }; expect(RelayOperationPreviewRequestSchema.parse(publish)).toEqual(publish); + expect(RelayOperationPreviewRequestSchema.safeParse({ ...publish, expectedRevisions: [draftTarget] }).success).toBe(true); for (const expectedRevisions of [ - [draftTarget], [memberTarget], [draftTarget, memberTarget, { target: { kind: "artifact" as const, path: "README.md" }, @@ -1260,6 +1262,28 @@ describe("Hub API contracts", () => { truncated: false, }; expect(WikiEntityListResponseSchema.safeParse(page).success).toBe(true); + const graph = { + indexedRevision: page.indexedRevision, observedAt: page.observedAt, + nodes: [summary], relations: [], + coverage: { nodeLimit: 100, relationLimit: 500, nodesTruncated: false, relationsTruncated: false }, + }; + expect(WikiGraphResponseSchema.safeParse(graph).success).toBe(true); + expect(WikiGraphResponseSchema.safeParse({ ...graph, nodes: [summary, summary] }).success).toBe(false); + expect(WikiGraphResponseSchema.safeParse({ ...graph, nodes: Array.from({ length: 101 }, (_, index) => ({ ...summary, id: `mx_${String(index).padStart(26, "0")}` })) }).success).toBe(false); + const relation = { type: "related_to", source: { id, kind: "architecture", title: null }, target: { id, kind: "architecture", title: null }, note: null }; + expect(WikiGraphResponseSchema.safeParse({ ...graph, relations: Array(501).fill(relation) }).success).toBe(false); + expect(WikiGraphResponseSchema.safeParse({ ...graph, relations: [{ ...relation, target: { ...relation.target, id: "mx_01K4FAM7W8N9R3T5Y6Q2ZBCHJE" } }] }).success).toBe(false); + const code = { + indexedRevision: page.indexedRevision, observedAt: page.observedAt, entityId: id, + graphRevision: null, groundings: [{ requestedNode: "function:queue", resolvedNode: null, health: "unverified", symbol: null }], truncated: false, + }; + expect(WikiGroundedCodeResponseSchema.safeParse(code).success).toBe(true); + expect(WikiGroundedCodeResponseSchema.safeParse({ ...code, groundings: Array(51).fill(code.groundings[0]) }).success).toBe(false); + const symbol = { id: "function:queue", symbolKind: "function", name: "queue", qualifiedName: "queue", language: "typescript", path: "src/queue.ts", startLine: 1, endLine: 4, route: "/code/symbols/function%3Aqueue" }; + const resolvedCode = { ...code, graphRevision: "c".repeat(64), groundings: [{ requestedNode: "function:queue", resolvedNode: "function:queue", health: "fresh", symbol }] }; + expect(WikiGroundedCodeResponseSchema.safeParse(resolvedCode).success).toBe(true); + expect(WikiGroundedCodeResponseSchema.safeParse({ ...resolvedCode, graphRevision: null }).success).toBe(false); + expect(WikiGroundedCodeResponseSchema.safeParse({ ...resolvedCode, groundings: [{ ...resolvedCode.groundings[0], resolvedNode: "function:other" }] }).success).toBe(false); expect(CodeKnowledgeResponseSchema.safeParse({ ...page, items: [{ entity: summary, matchedNodes: ["function:queue"] }], diff --git a/packages/hub-contracts/src/index.ts b/packages/hub-contracts/src/index.ts index 5486aefb..4336d23d 100644 --- a/packages/hub-contracts/src/index.ts +++ b/packages/hub-contracts/src/index.ts @@ -97,6 +97,19 @@ const hubJobId = z.string() .regex(/^job_[0-7][0-9A-HJKMNP-TV-Z]{25}$/, "Invalid Hub job ID."); const revision = z.string().regex(/^[a-f0-9]{64}$/, "Invalid SHA-256 revision."); +export const AgentLoggingModeSchema = z.enum(["significant", "checkpoints", "manual"]); +export const AgentLoggingPolicySchema = z.discriminatedUnion("source", [ + z.object({ mode: z.literal("significant"), revision: z.null(), source: z.literal("default") }).strict(), + z.object({ mode: AgentLoggingModeSchema, revision, source: z.literal("local") }).strict(), +]); +export const AgentLoggingUpdateRequestSchema = z.object({ + mode: AgentLoggingModeSchema, + expectedRevision: revision.nullable(), +}).strict(); +export type AgentLoggingMode = z.infer; +export type AgentLoggingPolicy = z.infer; +export type AgentLoggingUpdateRequest = z.infer; + /** Shared primitives for route-private contract entry points. */ export const HubIsoTimestampSchema = isoTimestamp; export const HubBoundedReasonSchema = boundedReason; @@ -623,6 +636,7 @@ const teamIdentityActivityAction = z.discriminatedUnion("kind", [ teamMemberAddAction, teamMemberUpdateAction, z.object({ kind: z.literal("member.deactivate"), memberId: teamMemberId }).strict(), + z.object({ kind: z.literal("member.reactivate"), memberId: teamMemberId }).strict(), z.object({ kind: z.literal("member.select"), memberId: teamMemberId }).strict(), z.object({ kind: z.literal("member.clear") }).strict(), z.object({ @@ -837,6 +851,12 @@ export const InboxSpecKindSchema = z.enum([ "acceptance_criterion", ]); +export const InboxKnowledgeKindSchema = z.enum([ + "architecture", "component", "convention", "decision", "pattern", "guide", +]); +export const InboxEntityKindSchema = z.union([InboxKnowledgeKindSchema, InboxSpecKindSchema]); +const inboxChangeKind = z.enum(["spec.create", "spec.update", "knowledge.create", "knowledge.update"]); + export const InboxProposalStateSchema = z.enum([ "pending", "approved", @@ -950,9 +970,23 @@ export const InboxSpecUpdateChangeSchema = z.object({ patch: inboxSpecUpdatePatch, }).strict(); +export const InboxKnowledgeCreateChangeSchema = inboxSpecCreateChangeObject + .omit({ relation: true }) + .extend({ kind: z.literal("knowledge.create"), entityKind: InboxKnowledgeKindSchema }) + .strict(); + +export const InboxKnowledgeUpdateChangeSchema = z.object({ + kind: z.literal("knowledge.update"), + target: inboxSpecRef.extend({ kind: InboxKnowledgeKindSchema }), + patch: inboxSpecUpdatePatch, +}).strict(); + +// Compatibility name: existing Spec envelopes retain their exact discriminators. export const InboxSpecChangeSchema = z.union([ InboxSpecCreateChangeSchema, InboxSpecUpdateChangeSchema, + InboxKnowledgeCreateChangeSchema, + InboxKnowledgeUpdateChangeSchema, ]); const inboxEntityRevisionExpectation = z.object({ @@ -1014,10 +1048,10 @@ export const InboxEvidenceRefSchema = z.discriminatedUnion("kind", [ ]); function inboxSpecDependencyIds(change: z.infer): string[] { - if (change.kind === "spec.update") return [change.target.id]; + if (change.kind === "spec.update" || change.kind === "knowledge.update") return [change.target.id]; return [...new Set([ ...(change.topics ?? []), - ...(change.relation ? [change.relation.target.id] : []), + ...(change.kind === "spec.create" && change.relation ? [change.relation.target.id] : []), ])].sort(); } @@ -1036,7 +1070,7 @@ function validateInboxDependencyCoverage( context.addIssue({ code: z.ZodIssueCode.custom, path: ["targetRevisions"], - message: "Exact revisions must cover every Spec dependency once.", + message: "Exact revisions must cover every knowledge dependency once.", }); } } @@ -1060,8 +1094,8 @@ export const InboxDraftSummarySchema = z.object({ id: InboxDraftIdSchema, revision, updatedAt: isoTimestamp, - changeKind: z.enum(["spec.create", "spec.update"]), - entityKind: InboxSpecKindSchema, + changeKind: inboxChangeKind, + entityKind: InboxEntityKindSchema, title: inboxText(512), rationaleExcerpt: utf8Text(240), }).strict(); @@ -1083,8 +1117,8 @@ const inboxProposalSummaryObject = z.object({ revision, state: InboxProposalStateSchema, author: TeamActorRefSchema, - changeKind: z.enum(["spec.create", "spec.update"]), - entityKind: InboxSpecKindSchema, + changeKind: inboxChangeKind, + entityKind: InboxEntityKindSchema, title: inboxText(512), rationaleExcerpt: utf8Text(240), reviewer: TeamActorRefSchema.optional(), @@ -1416,11 +1450,11 @@ const relayServiceActorRef = z.discriminatedUnion("kind", [ context.addIssue({ code: z.ZodIssueCode.custom, message: "A Relay service Git actor requires a name or email." }); } }); -const relayMemberSet = z.array(relayMemberRef).min(1).max(32).refine( +const relayMemberSet = z.array(relayMemberRef).max(32).refine( (values) => new Set(values.map((value) => value.memberId)).size === values.length, "Relay recipients must be unique Members.", ); -const relayRecordedRecipientSet = z.array(relayRecordedActorRef).min(1).max(64).refine( +const relayRecordedRecipientSet = z.array(relayRecordedActorRef).max(64).refine( (values) => new Set(values.map((value) => JSON.stringify(value))).size === values.length, "Recorded Relay recipients must be unique Actor references.", ); @@ -1559,6 +1593,7 @@ const relayRecordedEvidenceRef = z.discriminatedUnion("kind", [ const relayRecordedEvidenceList = z.array(relayRecordedEvidenceRef).max(65); const relayDraftInputObject = z.object({ + audience: z.enum(["team", "members"]).optional(), recipients: relayMemberSet, summary: relaySingleLineText(8 * 1024).refine((value) => value.length > 0, "Relay summary is required."), completed: relayTextList.default([]), @@ -1572,7 +1607,14 @@ const relayDraftInputObject = z.object({ nextActions: relayTextList.default([]), }).strict(); +function validateRelayDraftAudience(value: { audience?: "team" | "members"; recipients: readonly unknown[] }, context: z.RefinementCtx): void { + if (value.audience === "team" && value.recipients.length !== 0) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["recipients"], message: "Open-to-team Relays cannot also name recipients." }); + } +} + const RelayDraftInputSchema = relayDraftInputObject.superRefine((value, context) => { + validateRelayDraftAudience(value, context); if (value.evidence.length > 64) { context.addIssue({ code: z.ZodIssueCode.custom, @@ -1585,7 +1627,8 @@ const RelayDraftInputSchema = relayDraftInputObject.superRefine((value, context) } }); -const RelayDraftSummarySchema = z.object({ +const relayDraftSummaryObject = z.object({ + audience: z.enum(["team", "members"]).optional(), id: RelayDraftIdSchema, revision, updatedAt: isoTimestamp, @@ -1593,15 +1636,24 @@ const RelayDraftSummarySchema = z.object({ recipients: relayMemberSet, }).strict(); -const RelayDraftDetailSchema = RelayDraftSummarySchema.extend({ +const RelayDraftSummarySchema = relayDraftSummaryObject.superRefine(validateRelayDraftAudience); + +const RelayDraftDetailSchema = relayDraftSummaryObject.extend({ // Stored legacy drafts can grow slightly when sparse defaults are expanded // and Workstream is projected into reserved evidence. Reads stay field- // bounded; only caller-authored mutation input retains the 64 KiB ceiling. input: relayDraftInputObject, -}).strict(); +}).strict().superRefine((value, context) => { + validateRelayDraftAudience(value, context); + validateRelayDraftAudience(value.input, context); + if (value.audience !== value.input.audience) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["audience"], message: "The draft audience must match its input." }); + } +}); const relayDetailObject = z.object({ - schemaVersion: z.union([z.literal(1), z.literal(2), z.literal(3)]), + schemaVersion: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]), + audience: z.enum(["team", "members"]).optional(), ref: z.object({ id: RelayIdSchema, kind: z.literal("relay"), @@ -1635,7 +1687,8 @@ const relayDetailObject = z.object({ function validateRelayLifecycle( value: { - schemaVersion: 1 | 2 | 3; + schemaVersion: 1 | 2 | 3 | 4; + audience?: "team" | "members"; ref: { id: string }; sourcePath: string; state: "published" | "acknowledged" | "closed"; @@ -1657,6 +1710,13 @@ function validateRelayLifecycle( if (value.sourcePath !== `.mex/relays/${value.ref.id}.md`) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["sourcePath"], message: "Relay source path must match its ID." }); } + if (value.schemaVersion === 4) { + if (value.audience !== "team" || value.recipients.length !== 0) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["audience"], message: "Schema-v4 Relays must be open to team with no named recipients." }); + } + } else if (value.audience !== undefined || value.recipients.length === 0) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["recipients"], message: "Legacy Relays require named recipients and omit audience." }); + } if (value.schemaVersion === 1) { if (value.publishedAt !== null || value.publishedRepoState !== null || value.workstream === null) { context.addIssue({ code: z.ZodIssueCode.custom, message: "Schema-v1 Relays require a Workstream and omit publication time and repository state." }); @@ -1670,7 +1730,7 @@ function validateRelayLifecycle( } else if (value.publishedAt === null || value.publishedRepoState === null || value.workstream !== null) { context.addIssue({ code: z.ZodIssueCode.custom, message: "Schema-v3 Relays require publication time and repository state, without a Workstream." }); } - if (value.schemaVersion === 2 || value.schemaVersion === 3) { + if (value.schemaVersion >= 2) { const v2MemberIds = value.recipients.flatMap((recipient) => recipient.kind === "member" ? [recipient.memberId] : []); if (value.recipients.length > 32 || v2MemberIds.length !== value.recipients.length @@ -1698,7 +1758,7 @@ function validateRelayLifecycle( if ((value.evidence?.length ?? 0) > 64) { const first = value.evidence?.[0]; if ( - value.schemaVersion !== 3 + (value.schemaVersion !== 3 && value.schemaVersion !== 4) || first?.kind !== "entity" || first.entity.kind !== "workstream" || !teamWorkstreamId.safeParse(first.entity.id).success @@ -1738,6 +1798,7 @@ function validateRelayLifecycle( const RelayDetailSchema = relayDetailObject.superRefine(validateRelayLifecycle); const RelaySummarySchema = relayDetailObject.pick({ schemaVersion: true, + audience: true, ref: true, sourcePath: true, revision: true, @@ -1841,6 +1902,7 @@ const RelayOperationPreviewRequestSchema = z.object({ ? `local:${value.action.draftId}` : null; if (value.action.kind === "relay.draft.save") { + validateRelayDraftAudience(value.action.draft, context); if (value.action.draft.evidence.length > 64 && value.action.draftId === undefined) { context.addIssue({ code: z.ZodIssueCode.custom, @@ -1862,7 +1924,6 @@ const RelayOperationPreviewRequestSchema = z.object({ const legacyWorkstreamTargets = targets.filter((target) => /^artifact:\.mex\/workstreams\/ws_[0-7][0-9A-HJKMNP-TV-Z]{25}\.md$/.test(target)); if (localTargets.length !== 1 - || memberTargets.length < 1 || memberTargets.length > 32 || targets.length !== localTargets.length + memberTargets.length) { context.addIssue({ @@ -2737,6 +2798,48 @@ export const WikiRelationSchema = z.object({ note: wikiDisplayText(2_048, 0).nullable(), }).strict(); +/** A bounded whole-Context view, never a join of separately observed pages. */ +export const WikiGraphResponseSchema = z.object({ + indexedRevision: revision, + observedAt: isoTimestamp, + nodes: z.array(WikiEntitySummarySchema).max(100), + relations: z.array(WikiRelationSchema).max(500), + coverage: z.object({ + nodeLimit: z.literal(100), + relationLimit: z.literal(500), + nodesTruncated: z.boolean(), + relationsTruncated: z.boolean(), + }).strict(), +}).strict().superRefine((value, context) => { + const ids = new Set(value.nodes.map((node) => node.id)); + if (ids.size !== value.nodes.length) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["nodes"], message: "Context nodes must have distinct identities." }); + } + if (value.relations.some((relation) => !ids.has(relation.source.id) || !ids.has(relation.target.id))) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["relations"], message: "Context relationships must connect included nodes." }); + } +}); + +export const WikiGroundedCodeResponseSchema = z.object({ + indexedRevision: revision, + observedAt: isoTimestamp, + entityId: WikiEntityIdSchema, + graphRevision: revision.nullable(), + groundings: z.array(z.object({ + requestedNode: wikiDisplayText(512), + resolvedNode: wikiDisplayText(512).nullable(), + health: WikiGroundingHealthSchema, + symbol: GraphSymbolSchema.nullable(), + }).strict()).max(50), + truncated: z.boolean(), +}).strict().superRefine((value, context) => { + for (const grounding of value.groundings) { + if (grounding.symbol !== null && (value.graphRevision === null || grounding.symbol.id !== grounding.resolvedNode)) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["groundings"], message: "A code symbol requires its matching resolved identity and Graph revision." }); + } + } +}); + export const WikiRelationHitSchema = z.object({ direction: z.enum(["outgoing", "incoming"]), relation: WikiRelationSchema, @@ -3169,6 +3272,10 @@ export type TeamOperationApplyRequest = z.infer; export type TeamOperationApplyResponse = z.infer; export type InboxSpecKind = z.infer; +export type InboxKnowledgeKind = z.infer; +export type InboxEntityKind = z.infer; +export type InboxKnowledgeCreateChange = z.infer; +export type InboxKnowledgeUpdateChange = z.infer; export type InboxProposalState = z.infer; export type InboxDraftId = z.infer; export type InboxProposalId = z.infer; @@ -3253,6 +3360,8 @@ export type WikiRelationHit = z.infer; export type WikiRelationsRequest = z.infer; export type WikiBacklinksRequest = z.infer; export type WikiRelationsResponse = z.infer; +export type WikiGraphResponse = z.infer; +export type WikiGroundedCodeResponse = z.infer; export type WikiBacklinksResponse = z.infer; export type CodeKnowledgeRequest = z.infer; export type CodeKnowledgeHit = z.infer; diff --git a/packages/hub-contracts/src/relay-audience.test.ts b/packages/hub-contracts/src/relay-audience.test.ts new file mode 100644 index 00000000..bac60359 --- /dev/null +++ b/packages/hub-contracts/src/relay-audience.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { + RelayDetailSchema, RelayDraftDetailSchema, RelayDraftInputSchema, + RelayDraftSummarySchema, RelayOperationPreviewRequestSchema, RelaySummarySchema, + TeamOperationPreviewRequestSchema, +} from "./index.js"; + +const MEMBER = "member_01ARZ3NDEKTSV4RRFFQ69G5FAV"; +const RELAY = "relay_01000000000000000000000001"; +const member = { kind: "member" as const, memberId: MEMBER }; +const revision = "a".repeat(64); +const now = "2026-09-08T10:00:00.000Z"; +const draft = RelayDraftInputSchema.parse({ audience: "team", recipients: [], summary: "Resume this work later" }); +const canonical = { + schemaVersion: 4, audience: "team", ref: { id: RELAY, kind: "relay" }, + sourcePath: `.mex/relays/${RELAY}.md`, revision, state: "published", sender: member, + ...draft, workstream: null, diagnostics: [], diagnosticsTruncated: false, + publishedAt: now, publishedRepoState: { branch: "main", head: "b".repeat(40), dirty: false, observedAt: now }, + acknowledgedBy: null, acknowledgedAt: null, closedBy: null, closedAt: null, +}; + +describe("Relay audiences and Member recovery", () => { + it.each([undefined, "members", "team"] as const)("accepts a recipient-free local draft with audience %s", (audience) => { + const input = { ...draft, audience }; + expect(RelayDraftInputSchema.safeParse(input).success).toBe(true); + const summary = { audience, recipients: [], id: "draft-local", revision, updatedAt: now, summary: input.summary }; + expect(RelayDraftSummarySchema.safeParse(summary).success).toBe(true); + expect(RelayDraftDetailSchema.safeParse({ ...summary, input }).success).toBe(true); + expect(RelayOperationPreviewRequestSchema.safeParse({ operationId: "save-local", action: { kind: "relay.draft.save", draft: input }, expectedRevisions: [] }).success).toBe(true); + }); + + it("rejects team audiences carrying named recipients in every draft boundary", () => { + const input = { ...draft, recipients: [member] }; + const summary = { audience: "team", recipients: [member], id: "draft-local", revision, updatedAt: now, summary: draft.summary }; + expect(RelayDraftInputSchema.safeParse(input).success).toBe(false); + expect(RelayDraftSummarySchema.safeParse(summary).success).toBe(false); + expect(RelayDraftDetailSchema.safeParse({ ...summary, input }).success).toBe(false); + expect(RelayOperationPreviewRequestSchema.safeParse({ operationId: "save-local", action: { kind: "relay.draft.save", draft: input }, expectedRevisions: [] }).success).toBe(false); + }); + + it("requires schema-v4 team publication and preserves named schema-v3", () => { + const parsed = RelayDetailSchema.parse(canonical); + const { completed: _a, inProgress: _b, decisions: _c, blockers: _d, unresolvedQuestions: _e, changedFiles: _f, code: _g, evidence: _h, nextActions: _i, diagnostics: _j, diagnosticsTruncated: _k, ...summary } = parsed; + expect(RelaySummarySchema.parse(summary)).toEqual(summary); + for (const patch of [{ audience: undefined }, { audience: "members" }, { recipients: [member] }, { schemaVersion: 3 }, { publishedRepoState: null }, { sender: { kind: "unknown" } }]) { + expect(RelayDetailSchema.safeParse({ ...canonical, ...patch }).success).toBe(false); + } + const { audience: _audience, ...legacy } = canonical; + expect(RelayDetailSchema.safeParse({ ...legacy, schemaVersion: 3, recipients: [member] }).success).toBe(true); + expect(RelayDetailSchema.safeParse({ ...legacy, schemaVersion: 3, recipients: [] }).success).toBe(false); + }); + + it("binds reactivation to an existing target and rejects caller-owned authority", () => { + const request = { operationId: "reactivate-member", action: { kind: "member.reactivate", memberId: MEMBER }, expectedRevisions: [{ target: { kind: "artifact", path: `.mex/team/members/${MEMBER}.md` }, revision }] }; + expect(TeamOperationPreviewRequestSchema.parse(request)).toEqual(request); + expect(TeamOperationPreviewRequestSchema.safeParse({ ...request, expectedRevisions: [] }).success).toBe(false); + expect(TeamOperationPreviewRequestSchema.safeParse({ ...request, action: { ...request.action, actor: member } }).success).toBe(false); + }); +}); diff --git a/packages/hub-web/src/api/client.test.ts b/packages/hub-web/src/api/client.test.ts index c87a30ac..319fb20b 100644 --- a/packages/hub-web/src/api/client.test.ts +++ b/packages/hub-web/src/api/client.test.ts @@ -13,6 +13,7 @@ import { } from "./client"; import type { ActivityResponse, JobSummary, RelayOperationPreviewRequest } from "./types"; import { createFixtureApi } from "../dev/fixture-api"; +import type { HubTelemetryPage } from "./telemetry"; const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, @@ -51,6 +52,34 @@ afterEach(() => { vi.unstubAllGlobals(); }); +describe("local page telemetry transport", () => { + it("sends only a category with session CSRF to the exact local endpoint and bounds in-flight work", async () => { + let complete!: (response: Response) => void; + const pending = new Promise(resolve => { complete = resolve; }); + const fetch = vi.fn().mockResolvedValueOnce(json(session)).mockReturnValueOnce(pending); + vi.stubGlobal("fetch", fetch); + const api = new HttpHubApi(); + await api.recordPageView("home"); + expect(fetch).not.toHaveBeenCalled(); + await api.getSession(); + await api.recordPageView("/Users/private" as HubTelemetryPage); + expect(fetch).toHaveBeenCalledOnce(); + const sent = api.recordPageView("knowledge_detail"); + await api.recordPageView("code"); + expect(fetch).toHaveBeenCalledTimes(2); + expect(fetch.mock.calls[1]).toEqual(["/api/v1/telemetry/page", { + method: "POST", headers: { "Content-Type": "application/json", "X-MEX-CSRF": session.csrfToken }, + body: '{"page":"knowledge_detail"}', credentials: "same-origin", redirect: "error", + referrerPolicy: "no-referrer", signal: expect.any(AbortSignal), + }]); + complete(new Response(null, { status: 204 })); + await sent; + fetch.mockRejectedValueOnce(new Error("local connection lost")); + await expect(api.recordPageView("jobs")).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledTimes(3); + }); +}); + describe("bootstrap fragment handling", () => { it.each([ ["#token=secret-token", "secret-token"], diff --git a/packages/hub-web/src/api/client.ts b/packages/hub-web/src/api/client.ts index 1b1d0976..107030e9 100644 --- a/packages/hub-web/src/api/client.ts +++ b/packages/hub-web/src/api/client.ts @@ -1,4 +1,5 @@ import { + AgentLoggingPolicySchema, ActivityResponseSchema, BootstrapResponseSchema, CodeKnowledgeResponseSchema, @@ -31,6 +32,8 @@ import { TeamWorkstreamIdSchema, TeamWorkstreamListResponseSchema, TeamWorkstreamSchema, + WikiGraphResponseSchema, + WikiGroundedCodeResponseSchema, WikiBacklinksResponseSchema, WikiEntityDetailResponseSchema, WikiEntityIdSchema, @@ -39,6 +42,8 @@ import { } from "@mex/hub-contracts"; import { createFixtureApi } from "virtual:mex-hub-fixture-api"; import type { + AgentLoggingPolicy, + AgentLoggingUpdateRequest, ActivityRequest, ActivityResponse, BootstrapResponse, @@ -91,6 +96,8 @@ import type { TeamWorkstream, TeamWorkstreamListRequest, TeamWorkstreamListResponse, + WikiGraphResponse, + WikiGroundedCodeResponse, WikiBacklinksRequest, WikiBacklinksResponse, WikiEntityDetailResponse, @@ -100,6 +107,7 @@ import type { WikiRelationsResponse, } from "./types"; import type { RelayTransport } from "./relay-client"; +import { isHubTelemetryPage, type HubTelemetryPage } from "./telemetry"; const API_ROOT = "/api/v1"; @@ -158,6 +166,9 @@ export interface FixtureApiOptions { } export interface HubApi { + recordPageView?(page: HubTelemetryPage): Promise; + getLoggingPolicy(): Promise; + setLoggingPolicy(request: AgentLoggingUpdateRequest): Promise; bootstrap(token: string): Promise; getSession(): Promise; getCapabilities(): Promise; @@ -187,6 +198,8 @@ export interface HubApi { getActivity(request: ActivityRequest): Promise; search(request: SearchRequest): Promise; getCodeSymbol(id: string, request: CodeWorkspaceRequest): Promise; + wikiGraph(): Promise; + getWikiGroundedCode(id: string): Promise; listWikiEntities(request: WikiEntityListRequest): Promise; getWikiEntity(id: string): Promise; getWikiRelations(id: string, request: WikiRelationsRequest): Promise; @@ -309,6 +322,24 @@ export function clearBootstrapFragment(): void { export class HttpHubApi implements HubApi { #csrfToken: string | null = null; + #pageViewPending = false; + + async recordPageView(page: HubTelemetryPage): Promise { + if (!this.#csrfToken || this.#pageViewPending || !isHubTelemetryPage(page)) return; + this.#pageViewPending = true; + try { + await fetch(`${API_ROOT}/telemetry/page`, { + method: "POST", + headers: { "Content-Type": "application/json", "X-MEX-CSRF": this.#csrfToken }, + body: JSON.stringify({ page }), + credentials: "same-origin", + redirect: "error", + referrerPolicy: "no-referrer", + signal: AbortSignal.timeout(2_000), + }); + } catch { /* A dropped local usage event must never disturb navigation. */ } + finally { this.#pageViewPending = false; } + } #relayTransport: RelayTransport = { request: (path, schema, init, mutation) => this.#request(path, schema, init, mutation), invalidIdentifier: (detail) => { @@ -550,6 +581,14 @@ export class HttpHubApi implements HubApi { ); } + wikiGraph(): Promise { + return this.#request("/wiki/graph", WikiGraphResponseSchema); + } + + getWikiGroundedCode(id: string): Promise { + return this.#request(`/wiki/entities/${encodeURIComponent(assertSafeWikiEntityId(id))}/code`, WikiGroundedCodeResponseSchema); + } + listWikiEntities(request: WikiEntityListRequest): Promise { const params = new URLSearchParams({ limit: String(request.limit) }); if (request.kind) params.set("kind", request.kind); @@ -601,6 +640,15 @@ export class HttpHubApi implements HubApi { return this.#request("/health", HealthResponseSchema); } + getLoggingPolicy(): Promise { + return this.#request("/settings/logging", AgentLoggingPolicySchema); + } + + setLoggingPolicy(request: AgentLoggingUpdateRequest): Promise { + return this.#request("/settings/logging", AgentLoggingPolicySchema, + { method: "POST", body: JSON.stringify(request) }, true); + } + getJobs(cursor?: string): Promise { const params = new URLSearchParams({ limit: "25" }); if (cursor) params.set("cursor", cursor.slice(0, 4096)); diff --git a/packages/hub-web/src/api/telemetry.ts b/packages/hub-web/src/api/telemetry.ts new file mode 100644 index 00000000..27aafbf9 --- /dev/null +++ b/packages/hub-web/src/api/telemetry.ts @@ -0,0 +1,25 @@ +const PAGE_ROUTES = { + "/": "home", "/search": "search", "/knowledge": "knowledge", "/code": "code", + "/workstreams": "workstreams", "/specs": "specs", "/playbooks": "playbooks", + "/catch-up": "catch_up", "/inbox": "inbox", "/relays": "relays", "/members": "members", + "/activity": "activity", "/jobs": "jobs", "/health": "health", "/settings": "settings", +} as const; + +export type HubTelemetryPage = typeof PAGE_ROUTES[keyof typeof PAGE_ROUTES] + | "knowledge_detail" | "code_symbol" | "spec_detail" | "not_found"; + +const PAGES = new Set([...Object.values(PAGE_ROUTES), "knowledge_detail", "code_symbol", "spec_detail", "not_found"]); + +export function isHubTelemetryPage(page: unknown): page is HubTelemetryPage { + return typeof page === "string" && PAGES.has(page); +} + +/** Reduce locally before serialization; identifiers never enter the payload. */ +export function hubPageCategory(pathname: string): HubTelemetryPage { + const path = pathname.replace(/\/+$/, "") || "/"; + if (Object.hasOwn(PAGE_ROUTES, path)) return PAGE_ROUTES[path as keyof typeof PAGE_ROUTES]; + if (/^\/knowledge\/[^/]+$/.test(path)) return "knowledge_detail"; + if (/^\/code\/symbols\/[^/]+$/.test(path)) return "code_symbol"; + if (/^\/specs\/[^/]+$/.test(path)) return "spec_detail"; + return "not_found"; +} diff --git a/packages/hub-web/src/api/types.ts b/packages/hub-web/src/api/types.ts index 0de019fe..174e251d 100644 --- a/packages/hub-web/src/api/types.ts +++ b/packages/hub-web/src/api/types.ts @@ -1,4 +1,7 @@ export type { + AgentLoggingMode, + AgentLoggingPolicy, + AgentLoggingUpdateRequest, ActivityActor, ActivityDiagnostic, ActivityItem, @@ -51,6 +54,8 @@ export type { InboxProposalSummary, InboxSpecChange, InboxSpecKind, + InboxKnowledgeKind, + InboxEntityKind, RelayDetail, RelayDraftDetail, RelayDraftInput, @@ -112,6 +117,8 @@ export type { TeamWorkstreamListRequest, TeamWorkstreamListResponse, TeamWorkstreamState, + WikiGraphResponse, + WikiGroundedCodeResponse, WikiSearchResult, WikiBacklinksRequest, WikiBacklinksResponse, diff --git a/packages/hub-web/src/app/App.test.tsx b/packages/hub-web/src/app/App.test.tsx index 82620de3..f4370a31 100644 --- a/packages/hub-web/src/app/App.test.tsx +++ b/packages/hub-web/src/app/App.test.tsx @@ -28,7 +28,7 @@ describe("Project Hub routes", () => { it.each([ ["/", fixtureOverviewHeading], ["/search", "Search"], - ["/knowledge", "Knowledge"], + ["/knowledge", "Context"], ["/code", "Code"], ["/workstreams", "Workstreams"], ["/specs", "Specs"], @@ -68,37 +68,31 @@ describe("Project Hub routes", () => { const primary = within(sidebar).getByRole("navigation", { name: "Primary" }); expect(within(primary).getAllByRole("link").map((link) => link.querySelector("span")?.textContent)).toEqual([ "Overview", - "Knowledge", - "Specs", + "Context", "Code", - "Workstreams", "Inbox", "Relays", "Activity", + "Team", ]); - expect(within(primary).getByRole("region", { name: "Project Memory" })).toHaveTextContent( - "KnowledgeSpecsCode", + expect(within(primary).getByRole("region", { name: "Project" })).toHaveTextContent( + "ContextCode", ); expect(within(within(primary).getByRole("region", { name: "Teamwork" })) .getAllByRole("link") .map((link) => link.querySelector("span")?.textContent)) - .toEqual(["Workstreams", "Inbox", "Relays", "Activity"]); - const comingSoon = within(primary).getByRole("region", { name: "Coming Soon" }); - expect(within(comingSoon).queryByRole("link", { name: /Playbooks/u })).not.toBeInTheDocument(); - await user.click(within(comingSoon).getByRole("button", { name: "Coming Soon" })); - expect(within(comingSoon).getAllByRole("link").map((link) => link.querySelector("span")?.textContent)) - .toEqual(["Playbooks", "Catch Up"]); - expect(within(comingSoon).getByRole("link", { name: "Playbooks Soon" })).toHaveAttribute("href", "/playbooks"); - expect(within(comingSoon).getByRole("link", { name: "Catch Up Soon" })).toHaveAttribute("href", "/catch-up"); + .toEqual(["Relays", "Activity", "Team"]); + expect(within(primary).queryByRole("region", { name: "Coming Soon" })).not.toBeInTheDocument(); + for (const label of ["Specs", "Workstreams", "Playbooks", "Catch Up"]) { + expect(within(primary).queryByRole("link", { name: new RegExp(`^${label}`, "u") })).not.toBeInTheDocument(); + } const utilities = within(sidebar).getByRole("navigation", { name: "Project utilities" }); - expect(within(utilities).getAllByRole("link").map((link) => link.textContent)).toEqual([ - "Team", - ]); + expect(within(utilities).queryAllByRole("link")).toEqual([]); await user.click(within(utilities).getByRole("button", { name: /^System/u })); expect(within(utilities).getAllByRole("link").map((link) => link.querySelector("span")?.textContent)).toEqual([ - "Team", "Health", + "Settings", "Jobs", ]); }); @@ -108,38 +102,30 @@ describe("Project Hub routes", () => { const first = renderRoute("/"); await screen.findByRole("heading", { level: 1, name: fixtureOverviewHeading }); - const projectMemory = screen.getByRole("button", { name: "Project Memory" }); + const projectMemory = screen.getByRole("button", { name: "Project" }); const teamwork = screen.getByRole("button", { name: "Teamwork" }); - const comingSoon = screen.getByRole("button", { name: "Coming Soon" }); const system = screen.getByRole("button", { name: /^System/u }); expect(projectMemory).toHaveAttribute("aria-expanded", "true"); expect(teamwork).toHaveAttribute("aria-expanded", "true"); - expect(comingSoon).toHaveAttribute("aria-expanded", "false"); expect(system).toHaveAttribute("aria-expanded", "false"); await user.click(projectMemory); - await user.click(comingSoon); await user.click(system); expect(projectMemory).toHaveAttribute("aria-expanded", "false"); expect(teamwork).toHaveAttribute("aria-expanded", "true"); - expect(comingSoon).toHaveAttribute("aria-expanded", "true"); expect(system).toHaveAttribute("aria-expanded", "true"); first.unmount(); renderRoute("/"); await screen.findByRole("heading", { level: 1, name: fixtureOverviewHeading }); - expect(screen.getByRole("button", { name: "Project Memory" })).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByRole("button", { name: "Project" })).toHaveAttribute("aria-expanded", "true"); expect(screen.getByRole("button", { name: "Teamwork" })).toHaveAttribute("aria-expanded", "true"); - expect(screen.getByRole("button", { name: "Coming Soon" })).toHaveAttribute("aria-expanded", "false"); expect(screen.getByRole("button", { name: /^System/u })).toHaveAttribute("aria-expanded", "false"); }); it.each([ - ["/knowledge/mx_01K36WVM6H7JK8M9NPQRSTVVWX", "Project Memory", "Knowledge"], - ["/specs/mx_01K36WVM6H7JK8M9NPQRSTVVWX", "Project Memory", "Specs"], - ["/code/symbols/sym.createHubServer", "Project Memory", "Code"], - ["/playbooks", "Coming Soon", "Playbooks"], - ["/catch-up", "Coming Soon", "Catch Up"], + ["/knowledge/mx_01K36WVM6H7JK8M9NPQRSTVVWX", "Project", "Context"], + ["/code/symbols/sym.createHubServer", "Project", "Code"], ["/jobs", "System", "Jobs"], ])("opens the active group and marks its nested route for %s", async (route, group, link) => { renderRoute(route); @@ -152,11 +138,11 @@ describe("Project Hub routes", () => { it("keeps a manually collapsed active group visibly active", async () => { const user = userEvent.setup(); renderRoute("/knowledge/mx_01K36WVM6H7JK8M9NPQRSTVVWX"); - const disclosure = await screen.findByRole("button", { name: "Project Memory" }); + const disclosure = await screen.findByRole("button", { name: "Project" }); await user.click(disclosure); expect(disclosure).toHaveAttribute("aria-expanded", "false"); expect(disclosure).toHaveAttribute("data-active", "true"); - expect(screen.queryByRole("link", { name: "Knowledge" })).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "Context" })).not.toBeInTheDocument(); }); it("reopens a group when navigation enters one of its routes", async () => { @@ -285,7 +271,7 @@ describe("Project Hub routes", () => { await screen.findByRole("heading", { level: 1, name: fixtureOverviewHeading }); expect(within(screen.getByRole("link", { name: /Relays/u })).getByText("Unavailable")).toBeVisible(); - expect(within(screen.getByRole("link", { name: /^Inbox/u })).queryByText("Unavailable")).not.toBeInTheDocument(); + expect(within(screen.getByRole("link", { name: "Context" })).queryByText("Unavailable")).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /^System/u })); expect(within(screen.getByRole("link", { name: /Jobs/u })).getByText("Unavailable")).toBeVisible(); }); @@ -313,7 +299,7 @@ describe("Project Hub routes", () => { expect(await screen.findByText("Ada Lovelace")).toBeVisible(); const repositoryBar = screen.getByRole("banner", { name: "Repository context" }); expect(within(repositoryBar).getByText(initialHome.repository.name)).toBeVisible(); - expect(screen.getByLabelText("3 proposals awaiting team review.")).toBeVisible(); + expect(screen.getByLabelText("2 open Relays for you.")).toBeVisible(); // The Hub's only outbound link. `target="_blank"` without // `rel="noopener noreferrer"` would hand the opened tab a handle on this @@ -438,11 +424,10 @@ describe("Project Hub routes", () => { it("links Overview focus, team memory, context, and active operation to exact supported routes", async () => { renderRoute("/"); const focus = await screen.findByRole("region", { name: "Attention" }); - expect(within(focus).getByRole("button", { name: "Open Inbox" })).toHaveAttribute( - "href", - "/inbox?view=review&proposal=proposal_01000000000000000000001720", - ); - expect(within(focus).getByText("Take the handoff waiting for you").closest("a")).toHaveAttribute( + expect(screen.getByRole("button", { name: "Explore Context" })).toHaveAttribute("href", "/knowledge"); + expect(within(focus).getByRole("button", { name: "View Relays" })).toHaveAttribute("href", "/relays"); + expect(within(focus).queryByRole("button", { name: "Open Inbox" })).not.toBeInTheDocument(); + expect(within(focus).getByRole("button", { name: "Open handoff" })).toHaveAttribute( "href", "/relays?view=mine&state=open&relay=relay_01000000000000000000000001", ); diff --git a/packages/hub-web/src/app/App.tsx b/packages/hub-web/src/app/App.tsx index 489be9b6..007ad8ec 100644 --- a/packages/hub-web/src/app/App.tsx +++ b/packages/hub-web/src/app/App.tsx @@ -10,7 +10,7 @@ import { DesktopRequired, HubLayout } from "./HubLayout"; const HomePage = lazy(async () => ({ default: (await import("../pages/HomePage")).HomePage })); const SearchPage = lazy(async () => ({ default: (await import("../pages/SearchPage")).SearchPage })); const CodePage = lazy(async () => ({ default: (await import("../pages/SearchPage")).CodePage })); -const KnowledgePage = lazy(async () => ({ default: (await import("../pages/KnowledgePage")).KnowledgePage })); +const KnowledgePage = lazy(async () => ({ default: (await import("../pages/ContextPage")).ContextPage })); const KnowledgeDetailPage = lazy(async () => ({ default: (await import("../pages/KnowledgePage")).KnowledgeDetailPage })); const SymbolPage = lazy(async () => ({ default: (await import("../pages/SymbolPage")).SymbolPage })); const RoadmapPage = lazy(async () => ({ default: (await import("../pages/CapabilityPage")).RoadmapPage })); @@ -23,6 +23,7 @@ const InboxPage = lazy(async () => ({ default: (await import("../pages/InboxPage const RelayPage = lazy(async () => ({ default: (await import("../pages/RelayPage")).RelayPage })); const JobsPage = lazy(async () => ({ default: (await import("../pages/JobsPage")).JobsPage })); const HealthPage = lazy(async () => ({ default: (await import("../pages/HealthPage")).HealthPage })); +const SettingsPage = lazy(async () => ({ default: (await import("../pages/SettingsPage")).SettingsPage })); function SessionBoundary() { const session = useSession(); @@ -103,6 +104,7 @@ export function AppRoutes() { } /> } /> } /> + } /> } /> diff --git a/packages/hub-web/src/app/HubLayout.tsx b/packages/hub-web/src/app/HubLayout.tsx index cc73b849..f257a828 100644 --- a/packages/hub-web/src/app/HubLayout.tsx +++ b/packages/hub-web/src/app/HubLayout.tsx @@ -8,6 +8,7 @@ import { Button } from "../components/primitives/button"; import { formatTime, StatePanel, StatusPill } from "../components/ui"; import styles from "../styles/shell.module.css"; import { JobLifecycleObserver } from "./JobLifecycleObserver"; +import { PageViewObserver } from "./PageViewObserver"; import { HubSidebar } from "./HubSidebar"; /** @@ -181,6 +182,7 @@ export function HubLayout({ return (
+ {isOverview ? null : } Skip to main content diff --git a/packages/hub-web/src/app/HubSidebar.test.tsx b/packages/hub-web/src/app/HubSidebar.test.tsx index 888c3275..a370d3d6 100644 --- a/packages/hub-web/src/app/HubSidebar.test.tsx +++ b/packages/hub-web/src/app/HubSidebar.test.tsx @@ -49,11 +49,11 @@ describe("HubSidebar dynamic state", () => { const { capabilities, home } = await fixtureData(); renderSidebar({ capabilities, home: homeWithCounts(home, count) }); - const inbox = screen.getByRole("link", { name: /^Inbox/u }); + expect(screen.getByRole("link", { name: /^Inbox/u })).toBeVisible(); const relays = screen.getByRole("link", { name: /^Relays/u }); const system = screen.getByRole("button", { name: /^System/u }); const cases = [ - [inbox, `${count} proposals awaiting team review.`], + [screen.getByRole("link", { name: /^Inbox/u }), `${count} proposals for team review.`], [relays, `${count} open Relays for you.`], [system, `${count} active system operations.`], ] as const; @@ -72,7 +72,7 @@ describe("HubSidebar dynamic state", () => { const { capabilities, home } = await fixtureData(); const first = renderSidebar({ capabilities }); - expect(screen.getByRole("link", { name: "Inbox" })).toBeVisible(); + expect(screen.getByRole("link", { name: /^Inbox/u })).toBeVisible(); expect(screen.getByRole("link", { name: "Relays" })).toBeVisible(); expect(screen.getByRole("button", { name: "System" })).toBeVisible(); expect(screen.getByText("Set team identity")).toBeVisible(); @@ -89,7 +89,7 @@ describe("HubSidebar dynamic state", () => { }, }); - expect(screen.getByRole("link", { name: "Inbox" })).toBeVisible(); + expect(screen.getByRole("link", { name: /^Inbox/u })).toBeVisible(); expect(screen.getByRole("link", { name: "Relays" })).toBeVisible(); expect(screen.queryByText("Unavailable")).not.toBeInTheDocument(); }); @@ -97,29 +97,25 @@ describe("HubSidebar dynamic state", () => { it("lets capability unavailability win over counts and exposes its reason as a tooltip", async () => { const user = userEvent.setup(); const { capabilities, home } = await fixtureData(); - const reason = "Inbox reads are disconnected."; + const reason = "Relay reads are disconnected."; renderSidebar({ capabilities: { ...capabilities, - inbox: { - ...capabilities.inbox, - read: { availability: "unavailable", reason }, - }, relays: { ...capabilities.relays, - read: { availability: "unavailable", reason: "Relay reads are disconnected." }, + read: { availability: "unavailable", reason }, }, jobs: { availability: "unavailable", reason: "Job reads are disconnected." }, }, home: homeWithCounts(home, 9), }); - const inbox = screen.getByRole("link", { name: "Inbox Unavailable" }); - expect(inbox).toHaveAttribute("href", "/inbox"); - expect(inbox).toHaveAccessibleDescription(reason); - expect(within(inbox).getByText("Unavailable")).toBeVisible(); - expect(within(inbox).queryByLabelText("9 proposals awaiting team review.")).not.toBeInTheDocument(); - inbox.focus(); + const relays = screen.getByRole("link", { name: "Relays Unavailable" }); + expect(relays).toHaveAttribute("href", "/relays"); + expect(relays).toHaveAccessibleDescription(reason); + expect(within(relays).getByText("Unavailable")).toBeVisible(); + expect(within(relays).queryByLabelText("9 open Relays for you.")).not.toBeInTheDocument(); + relays.focus(); expect(await screen.findByRole("tooltip")).toHaveTextContent(reason); await user.keyboard("{Escape}"); expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); diff --git a/packages/hub-web/src/app/HubSidebar.tsx b/packages/hub-web/src/app/HubSidebar.tsx index 5b6a1380..382f6019 100644 --- a/packages/hub-web/src/app/HubSidebar.tsx +++ b/packages/hub-web/src/app/HubSidebar.tsx @@ -30,7 +30,7 @@ type ExpansionState = Record; const LOCALITY_EXPLANATION = "MEX runs on this device. Canonical team records are shared when committed and pushed; drafts and indexes remain local to this checkout."; const countLabels: Record string> = { - inbox: (count) => `${count} proposals awaiting team review.`, + inbox: (count) => `${count} proposals for team review.`, relays: (count) => `${count} open Relays for you.`, "active-jobs": (count) => `${count} active system operations.`, }; @@ -51,7 +51,6 @@ function initialExpansion(pathname: string): ExpansionState { }), { "project-memory": false, teamwork: false, - "coming-soon": false, system: false, }); } @@ -129,8 +128,6 @@ function NavigationLink({ ) : runtimeUnavailable ? ( Unavailable - ) : item.availability.kind === "coming-soon" ? ( - Soon ) : item.countSource ? ( countBadge(itemCount(home, item), item.countSource) ) : null} @@ -173,6 +170,7 @@ function NavigationList({ {items.map((item) => (
  • + {item.id === "team" ? {identityText(home)} : null}
  • ))} diff --git a/packages/hub-web/src/app/JobLifecycleObserver.test.tsx b/packages/hub-web/src/app/JobLifecycleObserver.test.tsx index 29f6c247..a47ac94d 100644 --- a/packages/hub-web/src/app/JobLifecycleObserver.test.tsx +++ b/packages/hub-web/src/app/JobLifecycleObserver.test.tsx @@ -1,5 +1,6 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"; import { act, render, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; import { describe, expect, it, vi } from "vitest"; import type { HubApi } from "../api/client"; import { HubApiProvider } from "../api/context"; @@ -32,17 +33,79 @@ class FakeBroadcastChannel { } } -function renderObserver(api: HubApi, queryClient: QueryClient, channelScope?: string) { +function renderObserver(api: HubApi, queryClient: QueryClient, channelScope?: string, children?: ReactNode) { return render( + {children} , ); } +const contextKeys = [ + ["wiki-graph"], + ["context-detail", "mx_selected", "same-wiki-revision"], + ["context-code", "mx_selected", "same-wiki-revision"], +] as const; + +function ContextRead({ queryKey, read }: { + queryKey: readonly string[]; + read(): Promise<{ indexedRevision: string; observation: number }>; +}) { + useQuery({ queryKey, queryFn: read, staleTime: Infinity }); + return null; +} + describe("JobLifecycleObserver", () => { + it.each(["graph_refresh", "graph_rebuild", "wiki_refresh", "wiki_rebuild"] as const)( + "refreshes mounted Context reads after successful %s even when the Wiki revision is unchanged", + async (kind) => { + const fixture = createFixtureApi(); + const jobs = await fixture.getJobs(); + const original = jobs.items.find((job) => job.state === "running"); + if (!original) throw new Error("Expected a running fixture job."); + let current = { ...original, kind }; + let publish: ((job: typeof original) => void) | undefined; + const subscribeToJob = vi.fn((_id: string, callback: (job: typeof original) => void) => { + publish = callback; + return { close() {} }; + }); + const api = { + getJobs: vi.fn(async () => ({ items: [current], nextCursor: null })), + subscribeToJob, + } as unknown as HubApi; + let observation = 0; + const readers = contextKeys.map(() => vi.fn(async () => ({ + indexedRevision: "same-wiki-revision", observation, + }))); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const rendered = renderObserver(api, queryClient, undefined, contextKeys.map((queryKey, index) => ( + + ))); + try { + await waitFor(() => { + expect(subscribeToJob).toHaveBeenCalled(); + for (const key of contextKeys) expect(queryClient.getQueryData(key)).toMatchObject({ observation: 0 }); + }); + await act(async () => publish?.({ ...current, progress: { completed: 2, total: 4 } })); + for (const read of readers) expect(read).toHaveBeenCalledOnce(); + + observation = 1; + current = { ...current, state: "succeeded", phase: "publish", finishedAt: "2026-08-26T12:01:00.000Z" }; + await act(async () => publish?.(current)); + await waitFor(() => { + for (const key of contextKeys) expect(queryClient.getQueryData(key)).toEqual({ indexedRevision: "same-wiki-revision", observation: 1 }); + }); + for (const read of readers) expect(read).toHaveBeenCalledTimes(2); + } finally { + rendered.unmount(); + queryClient.clear(); + } + }, + ); + it("loads lifecycle state once without registering a timer poll", async () => { const getJobs = vi.fn().mockResolvedValue({ items: [], nextCursor: null }); const subscribeToJob = vi.fn(); diff --git a/packages/hub-web/src/app/JobLifecycleObserver.tsx b/packages/hub-web/src/app/JobLifecycleObserver.tsx index 29d51dfc..29581f42 100644 --- a/packages/hub-web/src/app/JobLifecycleObserver.tsx +++ b/packages/hub-web/src/app/JobLifecycleObserver.tsx @@ -29,6 +29,9 @@ export async function invalidateGraphReads(queryClient: QueryClient): Promise { + it("records route categories once, omitting query/hash/identifiers and idle renders", async () => { + const recordPageView = vi.fn(async () => undefined); + const api = { ...createFixtureApi(), recordPageView }; + const router = createMemoryRouter([{ path: "*", element: }], { + initialEntries: ["/knowledge/mx_private?query=private%40example.test#private-token"], + }); + const rendered = render(); + await waitFor(() => expect(recordPageView).toHaveBeenCalledOnce()); + await act(() => router.navigate("/knowledge/mx_private?query=another-secret#another-token")); + expect(recordPageView).toHaveBeenCalledOnce(); + await act(() => router.navigate("/code/symbols/private-source-id?view=callers")); + await act(() => router.navigate("/code/symbols/another-source-id?view=callers")); + await act(() => router.navigate("/relays?relay=relay_private")); + await act(() => router.navigate("/unknown/private/path")); + rendered.rerender(); + expect(recordPageView.mock.calls).toEqual([ + ["knowledge_detail"], ["code_symbol"], ["code_symbol"], ["relays"], ["not_found"], + ]); + expect(JSON.stringify(recordPageView.mock.calls)).not.toMatch(/private|query|token|view|source-id/); + }); + + it("leaves fixtures quiet and ignores optional observer delivery failure", async () => { + const fetch = vi.spyOn(globalThis, "fetch"); + const router = createMemoryRouter([{ path: "*", element: }]); + const rendered = render(); + await act(() => router.navigate("/jobs")); + expect(fetch).not.toHaveBeenCalled(); + rendered.unmount(); + fetch.mockRestore(); + const recordPageView = vi.fn(async () => { throw new Error("local transport failed"); }); + const failingRouter = createMemoryRouter([{ path: "*", element: }]); + render(); + await waitFor(() => expect(recordPageView).toHaveBeenCalledOnce()); + await act(() => failingRouter.navigate("/settings")); + expect(recordPageView).toHaveBeenLastCalledWith("settings"); + }); +}); diff --git a/packages/hub-web/src/app/PageViewObserver.tsx b/packages/hub-web/src/app/PageViewObserver.tsx new file mode 100644 index 00000000..67610f5c --- /dev/null +++ b/packages/hub-web/src/app/PageViewObserver.tsx @@ -0,0 +1,17 @@ +import { useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; +import { useHubApi } from "../api/context"; +import { hubPageCategory } from "../api/telemetry"; + +/** Navigation only: query edits, hash changes, polling, and renders stay quiet. */ +export function PageViewObserver() { + const api = useHubApi(); + const { pathname } = useLocation(); + const previousPath = useRef(null); + useEffect(() => { + if (!api.recordPageView || previousPath.current === pathname) return; + previousPath.current = pathname; + void api.recordPageView(hubPageCategory(pathname)).catch(() => undefined); + }, [api, pathname]); + return null; +} diff --git a/packages/hub-web/src/app/context-page.test.tsx b/packages/hub-web/src/app/context-page.test.tsx new file mode 100644 index 00000000..c9fdfe41 --- /dev/null +++ b/packages/hub-web/src/app/context-page.test.tsx @@ -0,0 +1,207 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter, useLocation } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import { HubApiError, type HubApi } from "../api/client"; +import { HubApiProvider } from "../api/context"; +import { createFixtureApi } from "../dev/fixture-api"; +import { AppRoutes } from "./App"; + +const HUB_ID = "mx_01K36WVM6H7JK8M9NPQRSTVVWX"; +const HUB_NODE = "Project Hub read boundaries · architecture"; +const CODE_NODE = "createHubServer · code · fresh"; + +function LocationProbe() { + const location = useLocation(); + return {location.pathname}{location.search}; +} + +function renderContext(overrides: Partial = {}, route = "/knowledge") { + const api = Object.assign(createFixtureApi(), overrides); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); + return { api, queryClient, ...render( + + + + + + + + , + ) }; +} + +describe("Context graph page", () => { + it("shows all 30 entities, including disconnected sections, without requesting a list page or details", async () => { + const fixture = createFixtureApi(); + const graph = await fixture.wikiGraph(); + const extra = Array.from({ length: 27 }, (_, index) => ({ + ...graph.nodes[0], id: `mx_${String(index).padStart(26, "0")}`, kind: "component", + title: `Unlinked section ${index + 1}`, route: `/knowledge/mx_${String(index).padStart(26, "0")}`, + location: { path: ".mex/context/architecture.md", startLine: 50 + index * 3, endLine: 52 + index * 3 }, + })); + const wikiGraph = vi.fn(async () => ({ ...graph, nodes: [...graph.nodes, ...extra] })); + const listWikiEntities = vi.fn(fixture.listWikiEntities.bind(fixture)); + const getWikiEntity = vi.fn(fixture.getWikiEntity.bind(fixture)); + const getWikiGroundedCode = vi.fn(fixture.getWikiGroundedCode.bind(fixture)); + const getCodeSymbol = vi.fn(fixture.getCodeSymbol.bind(fixture)); + renderContext({ wikiGraph, listWikiEntities, getWikiEntity, getWikiGroundedCode, getCodeSymbol }); + + expect(await screen.findByRole("heading", { level: 1, name: "Context" })).toBeVisible(); + const canvas = await screen.findByLabelText("Context graph"); + for (const node of [...graph.nodes, ...extra]) { + expect(within(canvas).getByRole("button", { name: `${node.title} · ${node.kind}` })).toBeVisible(); + } + expect(screen.getByText(/30 entities · \d+ links/)).toBeVisible(); + expect(wikiGraph).toHaveBeenCalledOnce(); + expect(listWikiEntities).not.toHaveBeenCalled(); + expect(getWikiEntity).not.toHaveBeenCalled(); + expect(getWikiGroundedCode).not.toHaveBeenCalled(); + expect(getCodeSymbol).not.toHaveBeenCalled(); + }); + + it("switches between graph and the existing paginated list", async () => { + const user = userEvent.setup(); + const fixture = createFixtureApi(); + const listWikiEntities = vi.fn(fixture.listWikiEntities.bind(fixture)); + renderContext({ listWikiEntities }); + await screen.findByRole("button", { name: HUB_NODE }); + await user.click(screen.getByRole("button", { name: "List" })); + expect(await screen.findByRole("button", { name: "Load more Knowledge" })).toBeVisible(); + expect(screen.queryByLabelText("Context graph")).not.toBeInTheDocument(); + expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?view=list"); + expect(listWikiEntities).toHaveBeenCalledOnce(); + await user.click(screen.getByRole("button", { name: "Graph" })); + expect(await screen.findByRole("button", { name: HUB_NODE })).toBeVisible(); + expect(screen.getByRole("button", { name: "Graph" })).toHaveAttribute("aria-pressed", "true"); + }); + + it("reveals direct code on selection, preserves its Wiki context, and clears on Escape without fetching source bodies", async () => { + const user = userEvent.setup(); + const fixture = createFixtureApi(); + const getWikiEntity = vi.fn(fixture.getWikiEntity.bind(fixture)); + const getWikiGroundedCode = vi.fn(fixture.getWikiGroundedCode.bind(fixture)); + const getCodeSymbol = vi.fn(fixture.getCodeSymbol.bind(fixture)); + renderContext({ getWikiEntity, getWikiGroundedCode, getCodeSymbol }); + await user.click(await screen.findByRole("button", { name: HUB_NODE })); + const sidebar = within(screen.getByRole("complementary", { name: "Context details" })); + expect(await sidebar.findByText(/Every indexed response belongs to one stable revision/)).toBeVisible(); + expect(sidebar.getByRole("link", { name: "Open full record" })).toHaveAttribute("href", `/knowledge/${HUB_ID}`); + const satellite = await screen.findByRole("button", { name: CODE_NODE }); + await user.click(satellite); + expect(sidebar.getByRole("heading", { name: "createHubServer" })).toBeVisible(); + expect(sidebar.getByRole("link", { name: "Open in Code" })).toHaveAttribute("href", "/code/symbols/sym.createHubServer"); + expect(sidebar.getByText("createHubServer(options: HubServerOptions): Promise")).toBeVisible(); + expect(screen.getByRole("button", { name: HUB_NODE })).toHaveAttribute("aria-pressed", "true"); + expect(sidebar.getByRole("button", { name: "Back to Project Hub read boundaries" })).toBeVisible(); + await user.keyboard("{Escape}"); + expect(sidebar.getByRole("heading", { name: "Follow a connection" })).toBeVisible(); + expect(screen.queryByRole("button", { name: CODE_NODE })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: HUB_NODE })).toHaveAttribute("aria-pressed", "false"); + expect(getWikiEntity).toHaveBeenCalledExactlyOnceWith(HUB_ID); + expect(getWikiGroundedCode).toHaveBeenCalledExactlyOnceWith(HUB_ID); + expect(getCodeSymbol).not.toHaveBeenCalled(); + }); + + it("replaces code satellites when another Wiki entity is selected and distinguishes no grounding from unavailable code", async () => { + const user = userEvent.setup(); + renderContext(); + await user.click(await screen.findByRole("button", { name: HUB_NODE })); + await screen.findByRole("button", { name: CODE_NODE }); + await user.click(screen.getByRole("button", { name: "One snapshot per graph request · decision" })); + const sidebar = within(screen.getByRole("complementary", { name: "Context details" })); + expect(await sidebar.findByText("No explicit code grounding recorded.")).toBeVisible(); + expect(sidebar.queryByText("Code index unavailable. Groundings are unverified.")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: CODE_NODE })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "One snapshot per graph request · decision" })).toHaveAttribute("aria-pressed", "true"); + }); + + it.each(["detail", "code"] as const)("hides mismatched %s evidence and code until the graph is reloaded", async (mismatch) => { + const user = userEvent.setup(); + const fixture = createFixtureApi(); + renderContext({ + getWikiEntity: async (id) => { + const result = await fixture.getWikiEntity(id); + return mismatch === "detail" ? { ...result, indexedRevision: "f".repeat(64) } : result; + }, + getWikiGroundedCode: async (id) => { + const result = await fixture.getWikiGroundedCode(id); + return mismatch === "code" ? { ...result, indexedRevision: "f".repeat(64) } : result; + }, + }); + await user.click(await screen.findByRole("button", { name: HUB_NODE })); + const sidebar = within(screen.getByRole("complementary", { name: "Context details" })); + expect(await sidebar.findByRole("heading", { name: "Context changed" })).toBeVisible(); + expect(sidebar.queryByText(/Every indexed response belongs to one stable revision/)).not.toBeInTheDocument(); + expect(sidebar.queryByRole("link", { name: "Open in Code" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: CODE_NODE })).not.toBeInTheDocument(); + await user.click(sidebar.getByRole("button", { name: "Reload graph" })); + expect(await sidebar.findByRole("heading", { name: "Follow a connection" })).toBeVisible(); + }); + + it.each([ + ["missing", "b".repeat(64)], + ["unverified", null], + ] as const)("shows a %s grounding honestly without inventing a symbol", async (health, graphRevision) => { + const user = userEvent.setup(); + const fixture = createFixtureApi(); + renderContext({ getWikiGroundedCode: async (id) => ({ + ...await fixture.getWikiGroundedCode(id), graphRevision, + groundings: [{ requestedNode: "sym.unavailable", resolvedNode: null, health, symbol: null }], + }) }); + await user.click(await screen.findByRole("button", { name: HUB_NODE })); + const sidebar = within(screen.getByRole("complementary", { name: "Context details" })); + expect(await sidebar.findByText(`sym.unavailable · ${health} · code unavailable`)).toBeVisible(); + expect(screen.queryByRole("button", { name: / · code · / })).not.toBeInTheDocument(); + if (graphRevision === null) expect(sidebar.getByText("Code index unavailable. Groundings are unverified.")).toBeVisible(); + else expect(sidebar.queryByText("Code index unavailable. Groundings are unverified.")).not.toBeInTheDocument(); + }); + + it("keeps a safe sidebar error independent from the trusted graph and code projection", async () => { + const user = userEvent.setup(); + renderContext({ getWikiEntity: async () => { throw new Error("sqlite failure at /private/secret.db"); } }); + await user.click(await screen.findByRole("button", { name: HUB_NODE })); + const sidebar = within(screen.getByRole("complementary", { name: "Context details" })); + expect(await sidebar.findByRole("heading", { name: "Context unavailable" })).toBeVisible(); + expect(await screen.findByRole("button", { name: CODE_NODE })).toBeVisible(); + expect(screen.queryByText(/private\/secret/)).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: HUB_NODE })).toBeVisible(); + }); + + it("reports graph errors safely and still offers the list fallback", async () => { + const user = userEvent.setup(); + renderContext({ wikiGraph: async () => { throw new HubApiError({ type: "about:blank", title: "Stale Wiki", status: 409, code: "INDEX_STALE", detail: "Recorded knowledge changed. Inspect Health before refreshing.", requestId: "context_test" }); } }); + expect(await screen.findByRole("heading", { name: "The Context index is stale" })).toBeVisible(); + expect(screen.getByRole("link", { name: "Open Health" })).toHaveAttribute("href", "/health"); + await user.click(screen.getByRole("button", { name: "List" })); + expect(await screen.findByRole("button", { name: "Load more Knowledge" })).toBeVisible(); + }); + + it("reports a partial graph and keeps type filtering from hiding disconnected units", async () => { + const user = userEvent.setup(); + const fixture = createFixtureApi(); + const graph = await fixture.wikiGraph(); + renderContext({ wikiGraph: async () => ({ ...graph, relations: [], coverage: { ...graph.coverage, relationsTruncated: true } }) }); + expect(await screen.findByText(/Partial graph: up to 100 entities and 500 relationships are shown/)).toBeVisible(); + await user.click(screen.getByRole("button", { name: "Architecture" })); + expect(screen.getByRole("button", { name: "One snapshot per graph request · decision" })).toBeVisible(); + expect(screen.getByRole("button", { name: "One snapshot per graph request · decision" })).toHaveAttribute("data-dimmed", "true"); + await user.click(screen.getByRole("button", { name: "One snapshot per graph request · decision" })); + expect(await screen.findByText("No relationships in this graph.")).toBeVisible(); + }); + + it("keeps an unavailable Wiki capability from requesting graph or selected data", async () => { + const fixture = createFixtureApi(); + const capabilities = await fixture.getCapabilities(); + const wikiGraph = vi.fn(fixture.wikiGraph.bind(fixture)); + const getWikiEntity = vi.fn(fixture.getWikiEntity.bind(fixture)); + renderContext({ wikiGraph, getWikiEntity, getCapabilities: async () => ({ + ...capabilities, wiki: { ...capabilities.wiki, read: { availability: "unavailable", reason: "Wiki migration requires review." } }, + }) }); + expect(await screen.findByRole("heading", { name: "Context unavailable" })).toBeVisible(); + expect(screen.getByText("Wiki migration requires review.")).toBeVisible(); + expect(wikiGraph).not.toHaveBeenCalled(); + expect(getWikiEntity).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/hub-web/src/app/inbox-knowledge.test.tsx b/packages/hub-web/src/app/inbox-knowledge.test.tsx new file mode 100644 index 00000000..a3984ad9 --- /dev/null +++ b/packages/hub-web/src/app/inbox-knowledge.test.tsx @@ -0,0 +1,119 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import type { HubApi } from "../api/client"; +import { HubApiProvider } from "../api/context"; +import type { InboxOperationPreviewResponse, WikiEntityDetailResponse } from "../api/types"; +import { createFixtureApi } from "../dev/fixture-api"; +import { AppRoutes } from "./App"; + +function mount(api: HubApi, route = "/inbox?view=drafts") { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); + render( + + ); + return queryClient; +} + +async function createDialog(user: ReturnType) { + await user.click(await screen.findByRole("button", { name: "Create manually" })); + return screen.findByRole("dialog", { name: "Create local knowledge draft" }); +} + +describe("ordinary knowledge Inbox", () => { + it("offers the six knowledge kinds and saves one checkout-local pattern through the exact envelope", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const realPreview = api.previewInboxOperation.bind(api); + let envelope: InboxOperationPreviewResponse | undefined; + const preview = vi.spyOn(api, "previewInboxOperation").mockImplementation(async (request) => { + envelope = await realPreview(request); return envelope; + }); + const apply = vi.spyOn(api, "applyInboxOperation"); + mount(api); + const dialog = await createDialog(user); + expect(within(dialog).getByText(/private to this checkout/)).toBeVisible(); + const kind = within(dialog).getByRole("combobox", { name: "Knowledge kind" }); + expect(within(kind).getAllByRole("option").map((option) => option.getAttribute("value"))) + .toEqual(["architecture", "component", "convention", "decision", "pattern", "guide"]); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Title" }), { target: { value: "Preserve accepted evidence" } }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Summary (optional)" }), { target: { value: "Review drift explicitly." } }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Knowledge body" }), { target: { value: "Keep the old body.\n\tAccept only reviewed changes." } }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Rationale" }), { target: { value: "Successful execution is not review." } }); + await user.click(within(dialog).getByRole("button", { name: "Advanced" })); + expect(within(dialog).queryByRole("combobox", { name: /hierarchy relation/ })).not.toBeInTheDocument(); + await user.click(within(dialog).getByRole("button", { name: "Save draft" })); + await waitFor(() => expect(apply).toHaveBeenCalledOnce()); + expect(preview.mock.calls[0]![0].action).toMatchObject({ kind: "inbox.draft.save", draft: { + change: { kind: "knowledge.create", entityKind: "pattern", title: "Preserve accepted evidence", summary: "Review drift explicitly.", body: "Keep the old body.\n\tAccept only reviewed changes." }, + rationale: "Successful execution is not review.", targetRevisions: [], + } }); + expect(apply.mock.calls[0]![0]).toBe(envelope); + expect(envelope?.preview.scope).toBe("local"); + expect(envelope?.preview.changes).toEqual([]); + expect(await screen.findByRole("heading", { level: 2, name: "Preserve accepted evidence" })).toBeVisible(); + expect(screen.getByRole("button", { name: /Preserve accepted evidence/ })).toHaveAttribute("aria-current", "true"); + }); + + it("loads an ordinary Wiki target and preserves an explicit summary-only patch with exact revisions", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const target = (await api.listWikiEntities({ kind: "architecture", limit: 25 })).items[0]!; + const current = await api.getWikiEntity(target.id); + const list = vi.spyOn(api, "listWikiEntities"); + const detail = vi.spyOn(api, "getWikiEntity"); + const spec = vi.spyOn(api, "getSpec"); + const specs = vi.spyOn(api, "listSpecs"); + const preview = vi.spyOn(api, "previewInboxOperation"); + mount(api); + const dialog = await createDialog(user); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Change type" }), "update"); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Knowledge kind" }), "architecture"); + await within(dialog).findByRole("option", { name: target.title }); + expect(detail).not.toHaveBeenCalled(); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Knowledge to update" }), target.id); + await waitFor(() => expect(within(dialog).getByRole("textbox", { name: "Replacement body" })).toHaveValue(current.body.content)); + expect(list).toHaveBeenCalledWith({ kind: "architecture", limit: 25 }); + expect(detail).toHaveBeenCalledWith(target.id); + expect(spec).not.toHaveBeenCalled(); expect(specs).not.toHaveBeenCalled(); + await user.click(within(dialog).getByRole("button", { name: "Body" })); + await user.click(within(dialog).getByRole("button", { name: "Summary" })); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Replacement summary" }), { target: { value: "" } }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Rationale" }), { target: { value: "The body already carries the complete rationale." } }); + await user.click(within(dialog).getByRole("button", { name: "Save draft" })); + await waitFor(() => expect(preview).toHaveBeenCalledOnce()); + expect(preview.mock.calls[0]![0].action).toMatchObject({ kind: "inbox.draft.save", draft: { + change: { kind: "knowledge.update", target: { id: target.id, kind: "architecture", title: target.title }, patch: { summary: "" } }, + targetRevisions: [{ target: { kind: "entity", id: target.id }, revision: target.version.contentHash, semanticRevision: target.version.semanticRevision }], + } }); + }); + + it.each(["kind", "mode"])("discards a late target response after changing %s", async (change) => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const target = (await api.listWikiEntities({ kind: "architecture", limit: 25 })).items[0]!; + const current = await api.getWikiEntity(target.id); + let resolve!: (value: WikiEntityDetailResponse) => void; + vi.spyOn(api, "getWikiEntity").mockImplementation(() => new Promise((done) => { resolve = done; })); + mount(api); + const dialog = await createDialog(user); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Change type" }), "update"); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Knowledge kind" }), "architecture"); + await within(dialog).findByRole("option", { name: target.title }); + await user.selectOptions(within(dialog).getByRole("combobox", { name: "Knowledge to update" }), target.id); + expect(within(dialog).getByRole("textbox", { name: "Replacement body" })).toBeDisabled(); + await user.selectOptions(within(dialog).getByRole("combobox", { name: change === "kind" ? "Knowledge kind" : "Change type" }), change === "kind" ? "guide" : "create"); + await act(async () => { resolve(current); }); + if (change === "kind") { + expect(within(dialog).getByRole("combobox", { name: "Knowledge kind" })).toHaveValue("guide"); + expect(within(dialog).getByRole("combobox", { name: "Knowledge to update" })).toHaveValue(""); + } else { + expect(within(dialog).getByRole("combobox", { name: "Change type" })).toHaveValue("create"); + expect(within(dialog).getByRole("textbox", { name: "Knowledge body" })).toHaveValue(""); + expect(within(dialog).getByRole("textbox", { name: "Knowledge body" })).toBeEnabled(); + } + expect(within(dialog).getByRole("button", { name: "Save draft" })).toBeDisabled(); + }); +}); diff --git a/packages/hub-web/src/app/inbox-page.test.tsx b/packages/hub-web/src/app/inbox-page.test.tsx index 887c426b..dc2b614a 100644 --- a/packages/hub-web/src/app/inbox-page.test.tsx +++ b/packages/hub-web/src/app/inbox-page.test.tsx @@ -238,7 +238,7 @@ describe("Inbox Spec-authoring workbench", () => { renderRoute(createFixtureApi(), `/inbox?view=review&proposal=${OWN_PROPOSAL_ID}`); const detail = await screen.findByRole("region", { name: "Selected Inbox review detail" }); - expect(await within(detail).findByText("Spec change")).toBeVisible(); + expect(await within(detail).findByText("Knowledge change")).toBeVisible(); expect(within(detail).getByText("New constraint")).toBeVisible(); expect(within(detail).getByText("Published by Ada Lovelace")).toBeVisible(); expect(within(detail).getByRole("heading", { name: "What will change" })).toBeVisible(); @@ -336,7 +336,7 @@ describe("Inbox Spec-authoring workbench", () => { renderRoute(api, `/inbox?view=review&proposal=${PROPOSAL_ID}`); const detail = await screen.findByRole("region", { name: "Selected Inbox review detail" }); - expect(await within(detail).findByText("Current Spec content could not be read")).toBeVisible(); + expect(await within(detail).findByText("Current knowledge content could not be read")).toBeVisible(); expect(within(detail).getByText(/Proposed values remain available below/)).toBeVisible(); expect(within(detail).getByText("Require exact evidence review before release approval.")).toBeVisible(); expect(within(detail).getByText(/Private proposal prose remains outside durable Specs until approval/)).toBeVisible(); @@ -450,12 +450,12 @@ describe("Inbox Spec-authoring workbench", () => { renderRoute(api); await screen.findByRole("heading", { level: 2, name: "Clarify release evidence review" }); await user.click(screen.getByRole("button", { name: "Approve change" })); - const confirmation = await screen.findByRole("alertdialog", { name: "Approve this Spec change?" }); + const confirmation = await screen.findByRole("alertdialog", { name: "Approve this knowledge change?" }); expect(within(confirmation).getByText("Clarify release evidence review")).toBeVisible(); - expect(within(confirmation).getByText("Spec entity affected")).toBeVisible(); + expect(within(confirmation).getByText("Knowledge entity affected")).toBeVisible(); expect(within(confirmation).getByText("Human-team memory release")).toBeVisible(); expect(within(confirmation).getByText("Approving as Ada Lovelace")).toBeVisible(); - expect(within(confirmation).getByText("Write the reviewed Spec change")).toBeVisible(); + expect(within(confirmation).getByText("Write the reviewed knowledge change")).toBeVisible(); expect(within(confirmation).queryByText(".mex/specs/mx_01000000000000000000000001.md")).not.toBeInTheDocument(); expect(await openExactTechnicalDetails(user, confirmation)).toBeVisible(); @@ -528,7 +528,7 @@ describe("Inbox Spec-authoring workbench", () => { expect(preview).not.toHaveBeenCalled(); await user.click(within(warning).getByRole("button", { name: "Continue without teammate review" })); - const confirmation = await screen.findByRole("alertdialog", { name: "Approve this Spec change?" }); + const confirmation = await screen.findByRole("alertdialog", { name: "Approve this knowledge change?" }); expect(preview).toHaveBeenCalledOnce(); expect(preview.mock.calls[0]![0].action).toEqual({ kind: "inbox.approve", @@ -544,7 +544,7 @@ describe("Inbox Spec-authoring workbench", () => { const gitNotice = noticeTitle.closest("[role='alert']"); if (gitNotice === null) throw new Error("Expected the focused Git truth alert."); expect(within(gitNotice).getByText( - "Spec change and review record were written to your working tree. Commit and push them to share the result with your team.", + "Knowledge change and review record were written to your working tree. Commit and push them to share the result with your team.", )).toBeVisible(); await waitFor(() => expect(gitNotice).toHaveFocus()); await user.click(within(gitNotice).getByRole("button", { name: "Dismiss Git notice" })); @@ -557,8 +557,8 @@ describe("Inbox Spec-authoring workbench", () => { await screen.findByRole("heading", { level: 2, name: "Refresh the stale review boundary" }); expect(screen.getAllByText("Needs refresh").length).toBeGreaterThan(0); - expect(screen.getByText("The referenced Spec content changed after this proposal was published.")).toBeVisible(); - expect(screen.getByText("Its author or their agent should refresh this proposal against current Spec content.")).toBeVisible(); + expect(screen.getByText("The referenced knowledge content changed after this proposal was published.")).toBeVisible(); + expect(screen.getByText("Its author or their agent should refresh this proposal against current knowledge content.")).toBeVisible(); expect(screen.queryByRole("button", { name: "Approve change" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "More proposal actions" })).not.toBeInTheDocument(); expect(screen.queryByText("Repair manually…")).not.toBeInTheDocument(); @@ -727,10 +727,10 @@ describe("Inbox Spec-authoring workbench", () => { await openDrafts(user); const trigger = await screen.findByRole("button", { name: "Create manually" }); await user.click(trigger); - const dialog = await screen.findByRole("dialog", { name: "Create local Spec draft" }); + const dialog = await screen.findByRole("dialog", { name: "Create local knowledge draft" }); await waitFor(() => expect(within(dialog).getByRole("combobox", { name: "Change type" })).toHaveFocus()); const title = within(dialog).getByRole("textbox", { name: "Title" }); - const body = within(dialog).getByRole("textbox", { name: "Spec body" }); + const body = within(dialog).getByRole("textbox", { name: "Knowledge body" }); const rationale = within(dialog).getByRole("textbox", { name: "Rationale" }); fireEvent.change(title, { target: { value: "Canonical multiline requirement" } }); fireEvent.change(body, { target: { value: "First line\n\tTabbed second line" } }); @@ -824,9 +824,9 @@ describe("Inbox Spec-authoring workbench", () => { await openDrafts(user); const trigger = await screen.findByRole("button", { name: "Create manually" }); await user.click(trigger); - expect(await screen.findByRole("dialog", { name: "Create local Spec draft" })).toBeVisible(); + expect(await screen.findByRole("dialog", { name: "Create local knowledge draft" })).toBeVisible(); await user.keyboard("{Escape}"); - await waitFor(() => expect(screen.queryByRole("dialog", { name: "Create local Spec draft" })).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByRole("dialog", { name: "Create local knowledge draft" })).not.toBeInTheDocument()); expect(trigger).toHaveFocus(); }); @@ -845,10 +845,10 @@ describe("Inbox Spec-authoring workbench", () => { await openDrafts(user); await user.click(await screen.findByRole("button", { name: "Create manually" })); - const dialog = await screen.findByRole("dialog", { name: "Create local Spec draft" }); + const dialog = await screen.findByRole("dialog", { name: "Create local knowledge draft" }); const title = within(dialog).getByRole("textbox", { name: "Title" }); fireEvent.change(title, { target: { value: "Original reviewed title" } }); - fireEvent.change(within(dialog).getByRole("textbox", { name: "Spec body" }), { target: { value: "Original body." } }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Knowledge body" }), { target: { value: "Original body." } }); fireEvent.change(within(dialog).getByRole("textbox", { name: "Rationale" }), { target: { value: "Original rationale." } }); await user.click(within(dialog).getByRole("button", { name: "Save draft" })); await waitFor(() => expect(staleRequest).toBeDefined()); @@ -1092,8 +1092,8 @@ describe("Inbox Spec-authoring workbench", () => { await screen.findByRole("heading", { level: 2, name: draft.title }); await user.click(screen.getByRole("button", { name: "Publish for review" })); const confirmation = await screen.findByRole("alertdialog", { name: "Publish this draft for review?" }); - expect(within(confirmation).getByText(/Git-tracked proposal for teammate review/)).toBeVisible(); - expect(within(confirmation).getByText(/does not change the Spec or share anything automatically/)).toBeVisible(); + expect(within(confirmation).getByText(/pending Markdown proposal in this checkout/)).toBeVisible(); + expect(within(confirmation).getByText(/Project knowledge changes only after approval/)).toBeVisible(); expect(within(confirmation).getByText("Git step still required")).toBeVisible(); expect(preview).not.toHaveBeenCalled(); expect(apply).not.toHaveBeenCalled(); diff --git a/packages/hub-web/src/app/knowledge-pages.test.tsx b/packages/hub-web/src/app/knowledge-pages.test.tsx index 2a07ef92..6fa24cfc 100644 --- a/packages/hub-web/src/app/knowledge-pages.test.tsx +++ b/packages/hub-web/src/app/knowledge-pages.test.tsx @@ -40,9 +40,9 @@ function renderRoute(route: string, api: HubApi = createFixtureApi()) { describe("Read-only Knowledge browse", () => { it("renders real summaries and loads an older page without replacing trusted rows", async () => { const user = userEvent.setup(); - renderRoute("/knowledge"); + renderRoute("/knowledge?view=list"); - expect(await screen.findByRole("heading", { level: 1, name: "Knowledge" })).toBeVisible(); + expect(await screen.findByRole("heading", { level: 1, name: "Context" })).toBeVisible(); expect(await screen.findByText("Project Hub read boundaries")).toBeVisible(); expect(screen.getByText("One snapshot per graph request")).toBeVisible(); expect(screen.getByText("Some entries carry bounded diagnostics.")).toBeVisible(); @@ -54,26 +54,26 @@ describe("Read-only Knowledge browse", () => { it("keeps filters in URL history and makes text search an explicit independent mode", async () => { const user = userEvent.setup(); - renderRoute("/knowledge"); + renderRoute("/knowledge?view=list"); await screen.findByText("Project Hub read boundaries"); await user.selectOptions(screen.getByLabelText("Lifecycle"), "promoted"); await user.click(screen.getByRole("button", { name: "Apply" })); - await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?lifecycle=promoted")); + await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?view=list&lifecycle=promoted")); const filteredSummary = await screen.findByText("Filtered Knowledge records"); expect(filteredSummary.parentElement).toHaveFocus(); expect(screen.queryByText("Review immutable activity")).not.toBeInTheDocument(); await user.type(screen.getByRole("searchbox", { name: "Search titles, summaries, and bodies" }), "hub"); await user.click(screen.getByRole("button", { name: "Apply" })); - await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?q=hub")); + await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?view=list&q=hub")); expect(screen.getByTestId("location")).not.toHaveTextContent("lifecycle"); expect(await screen.findByText("Knowledge results for “hub”")).toBeVisible(); }); it("preserves a complete canonical topic ID through the URL-backed filter", async () => { const user = userEvent.setup(); - renderRoute("/knowledge"); + renderRoute("/knowledge?view=list"); await screen.findByText("Project Hub read boundaries"); const topic = screen.getByLabelText("Topic ID"); @@ -82,7 +82,7 @@ describe("Read-only Knowledge browse", () => { await user.click(screen.getByRole("button", { name: "Apply" })); await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent( - `/knowledge?topic=${GRAPH_TOPIC_ID}`, + `/knowledge?view=list&topic=${GRAPH_TOPIC_ID}`, )); expect(screen.getByText("Project Hub read boundaries")).toBeVisible(); expect(screen.queryByText("One snapshot per graph request")).not.toBeInTheDocument(); @@ -94,7 +94,7 @@ describe("Read-only Knowledge browse", () => { const listWikiEntities = vi.fn((request: Parameters[0]) => ( request.cursor ? Promise.reject(new Error("private path /tmp/wiki.sqlite")) : fixture.listWikiEntities(request) )); - renderRoute("/knowledge", apiWith({ listWikiEntities })); + renderRoute("/knowledge?view=list", apiWith({ listWikiEntities })); expect(await screen.findByText("Project Hub read boundaries")).toBeVisible(); await user.click(screen.getByRole("button", { name: "Load more Knowledge" })); @@ -112,13 +112,13 @@ describe("Read-only Knowledge browse", () => { const listWikiEntities = vi.fn((request: Parameters[0]) => ( request.cursor ? olderPage : fixture.listWikiEntities(request) )); - renderRoute("/knowledge", apiWith({ listWikiEntities })); + renderRoute("/knowledge?view=list", apiWith({ listWikiEntities })); await screen.findByText("Project Hub read boundaries"); await user.click(screen.getByRole("button", { name: "Load more Knowledge" })); await user.type(screen.getByLabelText("Kind"), "decision"); await user.click(screen.getByRole("button", { name: "Apply" })); - await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?kind=decision")); + await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?view=list&kind=decision")); expect(await screen.findByText("One snapshot per graph request")).toBeVisible(); expect(screen.queryByText("Project Hub read boundaries")).not.toBeInTheDocument(); @@ -133,14 +133,14 @@ describe("Read-only Knowledge browse", () => { it("distinguishes a successful filtered-empty read from an unavailable index", async () => { const user = userEvent.setup(); - renderRoute("/knowledge"); + renderRoute("/knowledge?view=list"); await screen.findByText("Project Hub read boundaries"); await user.type(screen.getByLabelText("Kind"), "nonexistent"); await user.click(screen.getByRole("button", { name: "Apply" })); expect(await screen.findByRole("heading", { name: "No Knowledge matches these filters" })).toBeVisible(); - expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?kind=nonexistent"); + expect(screen.getByTestId("location")).toHaveTextContent("/knowledge?view=list&kind=nonexistent"); }); it.each([ @@ -150,7 +150,7 @@ describe("Read-only Knowledge browse", () => { ["MIGRATION_REQUIRED", "Knowledge migration is required"], ] as const)("projects %s through the explicit Health boundary", async (code, title) => { const error = new HubApiError({ type: "about:blank", title, status: 409, code, detail: "Safe Wiki inspection failed.", requestId: `req_${code}` }); - renderRoute("/knowledge", apiWith({ listWikiEntities: () => Promise.reject(error) })); + renderRoute("/knowledge?view=list", apiWith({ listWikiEntities: () => Promise.reject(error) })); expect(await screen.findByRole("heading", { name: title })).toBeVisible(); expect(screen.getByRole("link", { name: "Open Wiki health" })).toHaveAttribute("href", "/health"); @@ -165,7 +165,7 @@ describe("Read-only Knowledge browse", () => { if (request.cursor && conflict) return { ...response, indexedRevision: "f".repeat(64) }; return response; }); - renderRoute("/knowledge", apiWith({ listWikiEntities })); + renderRoute("/knowledge?view=list", apiWith({ listWikiEntities })); await screen.findByText("Project Hub read boundaries"); await user.click(screen.getByRole("button", { name: "Load more Knowledge" })); @@ -186,7 +186,7 @@ describe("Read-only Knowledge browse", () => { wiki: { ...capabilities.wiki, read: { availability: "unavailable", reason: "Wiki migration must be reviewed manually." } }, }), }); - renderRoute("/knowledge", api); + renderRoute("/knowledge?view=list", api); expect(await screen.findByRole("heading", { name: "Knowledge unavailable" })).toBeVisible(); expect(screen.getByText("Wiki migration must be reviewed manually.")).toBeVisible(); }); @@ -201,8 +201,7 @@ describe("Read-only Knowledge browse", () => { expect(screen.getByRole("heading", { level: 2, name: `${title} is coming soon` })).toBeVisible(); expect(screen.getByText(copy)).toBeVisible(); expect(screen.getByText("Soon", { selector: '[data-slot="badge"][role="status"]' })).toBeVisible(); - expect(screen.getByRole("button", { name: "Coming Soon" })).toHaveAttribute("aria-expanded", "true"); - expect(screen.getByRole("link", { name: `${title} Soon` })).toHaveAttribute("aria-current", "page"); + expect(screen.queryByRole("button", { name: "Coming Soon" })).not.toBeInTheDocument(); expect(screen.queryByText("Read only")).not.toBeInTheDocument(); expect(screen.queryByText("No data requested")).not.toBeInTheDocument(); }); diff --git a/packages/hub-web/src/app/navigation.ts b/packages/hub-web/src/app/navigation.ts index 105119a8..fe9b96d2 100644 --- a/packages/hub-web/src/app/navigation.ts +++ b/packages/hub-web/src/app/navigation.ts @@ -1,30 +1,26 @@ import { Activity, - BookOpenText, Code2, - FileCheck2, GitPullRequestArrow, HeartPulse, - History, House, Inbox, - ListChecks, + Network, Search, Send, + Settings2, UsersRound, - Workflow, type LucideIcon, } from "lucide-react"; import type { CapabilityName } from "../api/types"; -export type NavigationGroupId = "project-memory" | "teamwork" | "coming-soon" | "system"; +export type NavigationGroupId = "project-memory" | "teamwork" | "system"; export type NavigationPlacement = "launcher" | "primary" | "footer"; export type NavigationCountSource = "inbox" | "relays" | "active-jobs"; export type NavigationAvailability = | { kind: "always" } - | { kind: "runtime"; capability: CapabilityName } - | { kind: "coming-soon" }; + | { kind: "runtime"; capability: CapabilityName }; export interface NavigationItem { id: string; @@ -46,9 +42,8 @@ export interface NavigationGroup { } export const navigationGroups: readonly NavigationGroup[] = [ - { id: "project-memory", label: "Project Memory", placement: "primary", defaultExpanded: true }, + { id: "project-memory", label: "Project", placement: "primary", defaultExpanded: true }, { id: "teamwork", label: "Teamwork", placement: "primary", defaultExpanded: true }, - { id: "coming-soon", label: "Coming Soon", placement: "primary", defaultExpanded: false }, { id: "system", label: "System", placement: "footer", defaultExpanded: false, countSource: "active-jobs" }, ]; @@ -71,22 +66,13 @@ export const navigationItems: readonly NavigationItem[] = [ }, { id: "knowledge", - label: "Knowledge", + label: "Context", path: "/knowledge", - icon: BookOpenText, + icon: Network, group: "project-memory", placement: "primary", availability: { kind: "runtime", capability: "wiki" }, }, - { - id: "specs", - label: "Specs", - path: "/specs", - icon: FileCheck2, - group: "project-memory", - placement: "primary", - availability: { kind: "runtime", capability: "specs" }, - }, { id: "code", label: "Code", @@ -96,21 +82,12 @@ export const navigationItems: readonly NavigationItem[] = [ placement: "primary", availability: { kind: "runtime", capability: "graph" }, }, - { - id: "workstreams", - label: "Workstreams", - path: "/workstreams", - icon: Workflow, - group: "teamwork", - placement: "primary", - availability: { kind: "runtime", capability: "workstreams" }, - }, { id: "inbox", label: "Inbox", path: "/inbox", icon: Inbox, - group: "teamwork", + group: "project-memory", placement: "primary", availability: { kind: "runtime", capability: "inbox" }, countSource: "inbox", @@ -134,30 +111,13 @@ export const navigationItems: readonly NavigationItem[] = [ placement: "primary", availability: { kind: "runtime", capability: "activity" }, }, - { - id: "playbooks", - label: "Playbooks", - path: "/playbooks", - icon: ListChecks, - group: "coming-soon", - placement: "primary", - availability: { kind: "coming-soon" }, - }, - { - id: "catch-up", - label: "Catch Up", - path: "/catch-up", - icon: History, - group: "coming-soon", - placement: "primary", - availability: { kind: "coming-soon" }, - }, { id: "team", label: "Team", path: "/members", icon: UsersRound, - placement: "footer", + group: "teamwork", + placement: "primary", availability: { kind: "runtime", capability: "members" }, }, { @@ -169,6 +129,15 @@ export const navigationItems: readonly NavigationItem[] = [ placement: "footer", availability: { kind: "always" }, }, + { + id: "settings", + label: "Settings", + path: "/settings", + icon: Settings2, + group: "system", + placement: "footer", + availability: { kind: "always" }, + }, { id: "jobs", label: "Jobs", diff --git a/packages/hub-web/src/app/page-states.test.tsx b/packages/hub-web/src/app/page-states.test.tsx index 2e683003..5554e355 100644 --- a/packages/hub-web/src/app/page-states.test.tsx +++ b/packages/hub-web/src/app/page-states.test.tsx @@ -61,13 +61,12 @@ describe("Home states", () => { it("renders established focus, semantic team memory, stale context, and an exact active operation", async () => { renderRoute("/", createFixtureApi({ overviewFixture: "established" })); - expect(await screen.findByRole("heading", { name: "Review 3 proposed Spec changes" })).toBeVisible(); + expect(await screen.findByRole("heading", { name: "Take the handoff waiting for you" })).toBeVisible(); + expect(screen.queryByText("Review 3 proposed Spec changes")).not.toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Attention" })).toBeVisible(); - expect(screen.getByRole("button", { name: "Open Inbox" })).toHaveAttribute( - "href", - "/inbox?view=review&proposal=proposal_01000000000000000000001720", - ); - expect(screen.getByText("Take the handoff waiting for you").closest("a")).toHaveAttribute( + expect(screen.getByRole("button", { name: "Explore Context" })).toHaveAttribute("href", "/knowledge"); + expect(screen.getByRole("button", { name: "View Relays" })).toHaveAttribute("href", "/relays"); + expect(screen.getByRole("button", { name: "Open handoff" })).toHaveAttribute( "href", "/relays?view=mine&state=open&relay=relay_01000000000000000000000001", ); @@ -162,10 +161,8 @@ describe("Home states", () => { await user.click(screen.getByRole("button", { name: "View technical details for Attention" })); expect(screen.getByText("RELAY_LEGACY_PUBLICATION_TIME")).toBeVisible(); expect(screen.getByText("Relay diagnostics truncated").nextElementSibling).toHaveTextContent("Yes"); - expect(screen.getByText("INBOX_SOURCE_TRUNCATED")).toBeVisible(); - expect(screen.getByText("Inbox corpus truncated").nextElementSibling).toHaveTextContent("Yes"); - expect(screen.getByText("Inbox source truncated").nextElementSibling).toHaveTextContent("Yes"); - expect(screen.getByText("Inbox diagnostics truncated").nextElementSibling).toHaveTextContent("Yes"); + expect(screen.queryByText("Inbox focus unavailable")).not.toBeInTheDocument(); + expect(screen.queryByText("INBOX_SOURCE_TRUNCATED")).not.toBeInTheDocument(); }); it("renders detached and unborn Graph repository observations without shell fallback", async () => { @@ -197,7 +194,7 @@ describe("Home states", () => { expect(await screen.findByText("You’re caught up")).toBeVisible(); expect(screen.getByRole("heading", { name: "Overview" })).toBeVisible(); - expect(screen.getByRole("button", { name: "Browse project memory" })).toHaveAttribute("href", "/search"); + expect(screen.getByRole("button", { name: "Browse shared knowledge" })).toHaveAttribute("href", "/knowledge"); expect(screen.getAllByText("Fresh", { selector: "dd" })).toHaveLength(2); expect(screen.getAllByText("Fresh", { selector: "strong" })).toHaveLength(2); expect(screen.queryByRole("heading", { name: "Active operation" })).not.toBeInTheDocument(); @@ -254,7 +251,7 @@ describe("Home states", () => { expect(await screen.findByText("Resolve who you’re working as")).toBeVisible(); expect(screen.getByText("The referenced member no longer exists.")).toBeVisible(); expect(screen.getByRole("button", { name: "Review identity" })).toHaveAttribute("href", "/members"); - expect(screen.getByText("Review 3 proposed Spec changes")).toBeVisible(); + expect(screen.queryByText("Review 3 proposed Spec changes")).not.toBeInTheDocument(); }); it("renders degraded parse composition and determinate or indeterminate operations exactly", async () => { @@ -267,6 +264,7 @@ describe("Home states", () => { const determinateProgress = within(await screen.findByRole("region", { name: "Active operation" })).getByRole("progressbar"); expect(determinateProgress).toHaveAccessibleName("Graph refresh · Parse"); expect(determinateProgress).toHaveAttribute("aria-valuenow", "68"); + expect(screen.getByText("124 / 183 files parsed")).toBeVisible(); determinate.unmount(); renderRoute("/", createFixtureApi({ overviewFixture: "job-indeterminate" })); @@ -276,13 +274,32 @@ describe("Home states", () => { expect(screen.getByText("37 completed")).toBeVisible(); }); + it.each(["resolve", "validate", "publish"] as const)("keeps Overview indeterminate after parsing during %s", async (phase) => { + const fixture = createFixtureApi({ overviewFixture: "job-determinate" }); + const overview = await fixture.getOverview(); + if (overview.operation.availability !== "available" || !overview.operation.active) { + throw new Error("The determinate fixture must supply an active graph operation."); + } + overview.operation.active = { + ...overview.operation.active, + phase, + progress: { completed: 183, total: 183 }, + }; + renderRoute("/", apiWith({ getOverview: async () => overview })); + const operation = within(await screen.findByRole("region", { name: "Active operation" })); + expect(operation.getByText("183 / 183 files parsed")).toBeVisible(); + const progress = operation.getByRole("progressbar"); + expect(progress).toHaveAccessibleName(`Graph refresh · ${phase[0].toUpperCase()}${phase.slice(1)}`); + expect(progress).not.toHaveAttribute("aria-valuenow"); + }); + it("shows only the bounded relevant failed operation when no newer success supersedes it", async () => { renderRoute("/", createFixtureApi({ overviewFixture: "failure" })); expect(await screen.findByRole("heading", { name: "Operation needs attention" })).toBeVisible(); const progress = within(screen.getByRole("region", { name: "Operation needs attention" })).getByRole("progressbar"); expect(progress).toHaveAccessibleName("Graph refresh · Failed"); - expect(progress).toHaveAttribute("aria-valuenow", "96"); + expect(progress).not.toHaveAttribute("aria-valuenow"); expect(screen.getByText("The previous trustworthy Graph index was preserved.")).toBeVisible(); expect(within(screen.getByRole("region", { name: "Operation needs attention" })) .getByRole("button", { name: "View operation" })).toHaveAttribute( @@ -963,6 +980,19 @@ describe("Health states", () => { }); describe("Jobs states", () => { + it.each(["parse", "resolve", "validate", "publish"] as const)("shows parsed files without claiming overall completion during %s", async (phase) => { + const fixture = createFixtureApi(); + const original = (await fixture.getJobs()).items[0]!; + const job: JobSummary = { ...original, kind: "graph_refresh", state: "running", phase, progress: { completed: 10, total: 10 } }; + renderRoute("/jobs", apiWith({ + getJobs: async () => ({ items: [job], nextCursor: null }), + subscribeToJob: () => ({ close: () => undefined }), + })); + expect(await screen.findByText("10 / 10 files parsed")).toBeVisible(); + if (phase === "parse") expect(screen.getByRole("progressbar", { name: "100% of files parsed" })).toBeVisible(); + else expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); + }); + it("renders its loading state while persisted summaries are pending", async () => { renderRoute("/jobs", apiWith({ getJobs: () => pending() })); diff --git a/packages/hub-web/src/app/relay-page.test.tsx b/packages/hub-web/src/app/relay-page.test.tsx index a49360c3..57e128be 100644 --- a/packages/hub-web/src/app/relay-page.test.tsx +++ b/packages/hub-web/src/app/relay-page.test.tsx @@ -530,7 +530,7 @@ describe("Relay handoff workbench", () => { await userEvent.setup().click(await screen.findByRole("button", { name: "Take handoff" })); const dialog = await screen.findByRole("alertdialog", { name: "Take this handoff?" }); expect(within(dialog).getByText(/sole claimant/i)).toBeVisible(); - expect(within(dialog).getByText(/other eligible recipients.*no longer.*take/i)).toBeVisible(); + expect(within(dialog).getByText(/other eligible Members.*no longer.*take/i)).toBeVisible(); expect(within(dialog).getByText(/no unclaim or reassignment/i)).toBeVisible(); expect(within(dialog).getByText(/pull the latest repository state/i)).toBeVisible(); expect(within(dialog).getByText("Release evidence is ready for the final cross-platform gate.")).toBeVisible(); @@ -567,7 +567,7 @@ describe("Relay handoff workbench", () => { expect(await screen.findByRole("heading", { name: relay.summary })).toBeVisible(); expect(screen.queryByRole("button", { name: "Take handoff" })).not.toBeInTheDocument(); expect(screen.getByText(/This handoff is addressed to Grace Hopper/i)).toBeVisible(); - expect(screen.getByText(/A listed recipient can take it after MEX resolves them to an active team identity/i)).toBeVisible(); + expect(screen.getByText(/MEX must resolve an eligible, active team identity before taking it/i)).toBeVisible(); expect(preview).not.toHaveBeenCalled(); }); @@ -668,7 +668,7 @@ describe("Relay handoff workbench", () => { renderRoute(api, `/relays?view=${view}&state=open&relay=${RELAY_ID}`); expect(await screen.findByRole("heading", { name: relay.summary })).toBeVisible(); - await waitFor(() => expect(screen.getByText(/Grace Hopper.*no longer an active team Member.*cannot be closed/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Grace Hopper.*no longer an active team Member.*Reactivate/i)).toBeVisible()); expect(screen.getByRole("button", { name: "Close handoff" })).toBeDisabled(); }); @@ -715,7 +715,7 @@ describe("Relay handoff workbench", () => { const dialog = await screen.findByRole("alertdialog", { name: "Close this handoff?" }); expect(within(dialog).getByText(/closing is irreversible/i)).toBeVisible(); expect(within(dialog).getByText(/removes (?:this|the) handoff from open attention/i)).toBeVisible(); - expect(within(dialog).getByText(/does not (?:complete|modify).*(?:Workstream|task)/i)).toBeVisible(); + expect(within(dialog).getByText(/saved handoff remains readable in project history/i)).toBeVisible(); const technical = within(dialog).getByRole("button", { name: "Technical details" }); expect(technical).toHaveAttribute("aria-expanded", "false"); @@ -1393,6 +1393,79 @@ describe("Relay handoff workbench", () => { expect(apply).not.toHaveBeenCalled(); }); + it("saves an open-to-team draft without Member reads, then publishes only after exact review", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const members = vi.spyOn(api, "getMembers"); + const member = vi.spyOn(api, "getMember"); + const preview = vi.spyOn(api, "previewRelayOperation"); + const apply = vi.spyOn(api, "applyRelayOperation"); + renderRoute(api, "/relays?view=drafts"); + await user.click(await screen.findByRole("button", { name: "Create manually" })); + const composer = await screen.findByRole("dialog", { name: "Create handoff draft" }); + expect(within(composer).getByRole("combobox", { name: "Who can take this handoff?" })).toHaveValue("team"); + expect(within(composer).queryByRole("combobox", { name: "Eligible recipients" })).not.toBeInTheDocument(); + fireEvent.change(within(composer).getByRole("textbox", { name: "Summary" }), { target: { value: "Context for whoever joins this work next" } }); + await user.click(within(composer).getByRole("button", { name: "Save draft" })); + await waitFor(() => expect(apply).toHaveBeenCalledTimes(1)); + expect(preview.mock.calls[0]![0]).toMatchObject({ action: { kind: "relay.draft.save", draft: { audience: "team", recipients: [] } }, expectedRevisions: [] }); + expect(apply.mock.calls[0]![0]).toBe(await preview.mock.results[0]!.value); + await waitFor(() => expect(routeLocation()).toContain("draft=relay-draft-02")); + expect(await screen.findByText(/Saved only in this checkout/)).toBeVisible(); + expect(members).not.toHaveBeenCalled(); + expect(member).not.toHaveBeenCalled(); + await user.click(await screen.findByRole("button", { name: "Publish handoff" })); + const review = await screen.findByRole("alertdialog", { name: "Publish this handoff?" }); + const confirm = within(review).getByRole("button", { name: "Publish handoff" }); + await waitFor(() => expect(confirm).toBeEnabled()); + expect(within(review).getByText(/Any active Member, including teammates who join later/)).toBeVisible(); + expect(apply).toHaveBeenCalledTimes(1); + expect(preview.mock.calls[1]![0].expectedRevisions).toEqual([{ target: { kind: "local", namespace: "relay-draft", id: "relay-draft-02" }, revision: "c".repeat(64) }]); + expect(members).not.toHaveBeenCalled(); + expect(member).not.toHaveBeenCalled(); + await user.click(confirm); + await waitFor(() => expect(apply).toHaveBeenCalledTimes(2)); + expect(apply.mock.calls[1]![0]).toBe(await preview.mock.results[1]!.value); + expect(await screen.findByText("Published working-tree artifact.")).toBeVisible(); + expect(screen.getByText(/MEX has not verified commit, push, or receipt/)).toBeVisible(); + expect(screen.getByText(/Any active Member can take this handoff, including teammates who join later/)).toBeVisible(); + }); + + it("allows a recipient-free named local draft but requires choosing an audience before publication", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const apply = vi.spyOn(api, "applyRelayOperation"); + renderRoute(api, "/relays?view=drafts"); + await user.click(await screen.findByRole("button", { name: "Create manually" })); + const composer = await screen.findByRole("dialog", { name: "Create handoff draft" }); + await user.selectOptions(within(composer).getByRole("combobox", { name: "Who can take this handoff?" }), "members"); + fireEvent.change(within(composer).getByRole("textbox", { name: "Summary" }), { target: { value: "Choose a recipient later" } }); + await user.click(within(composer).getByRole("button", { name: "Save draft" })); + await waitFor(() => expect(apply).toHaveBeenCalledTimes(1)); + expect(await screen.findByText(/Choose named Members or open this handoff to the team/)).toBeVisible(); + expect(screen.getByRole("button", { name: "Publish handoff" })).toBeDisabled(); + }); + + it("allows a newly joined active Member to review taking an open-to-team Relay", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const existing = await api.getRelay(RELAY_ID); + const relay: RelayDetail = { ...existing, schemaVersion: 4, audience: "team", recipients: [], summary: "Open handoff created before the teammate joined" }; + mockSingleRelay(api, relay); + mockCurrentMember(api, "member_05000000000000000000000001", "New teammate"); + const preview = vi.spyOn(api, "previewRelayOperation"); + const apply = vi.spyOn(api, "applyRelayOperation"); + renderRoute(api, `/relays?view=mine&relay=${RELAY_ID}`); + const take = await screen.findByRole("button", { name: "Take handoff" }); + expect(take).toBeEnabled(); + expect(screen.getByText(/including teammates who join later/)).toBeVisible(); + await user.click(take); + const review = await screen.findByRole("alertdialog", { name: "Take this handoff?" }); + await waitFor(() => expect(within(review).getByRole("button", { name: "Take handoff" })).toBeEnabled()); + expect(preview.mock.calls[0]![0]).toMatchObject({ action: { kind: "relay.acknowledge", relayId: RELAY_ID }, expectedRevisions: [{ target: { kind: "artifact", path: relay.sourcePath }, revision: relay.revision }] }); + expect(apply).not.toHaveBeenCalled(); + }); + it("saves a standalone sparse draft without any Workstream query or payload field", async () => { const user = userEvent.setup(); const api = createFixtureApi(); @@ -1406,6 +1479,7 @@ describe("Relay handoff workbench", () => { await waitFor(() => expect(trigger).toBeEnabled()); await user.click(trigger); const composer = await screen.findByRole("dialog", { name: "Create handoff draft" }); + await user.selectOptions(within(composer).getByRole("combobox", { name: "Who can take this handoff?" }), "members"); const recipients = within(composer).getByRole("combobox", { name: "Eligible recipients" }); await user.type(recipients, "Grace"); await user.click(await screen.findByRole("option", { name: "Grace Hopper" })); @@ -1419,6 +1493,7 @@ describe("Relay handoff workbench", () => { expect(request.action).toEqual({ kind: "relay.draft.save", draft: { + audience: "members", recipients: [{ kind: "member", memberId: GRACE_ID, displayName: "Grace Hopper" }], summary: "Standalone local handoff for a teammate.", completed: [], @@ -1455,6 +1530,7 @@ describe("Relay handoff workbench", () => { await user.click(await screen.findByRole("button", { name: "Create manually" })); const composer = await screen.findByRole("dialog", { name: "Create handoff draft" }); + await user.selectOptions(within(composer).getByRole("combobox", { name: "Who can take this handoff?" }), "members"); await user.click(within(composer).getByRole("button", { name: "Advanced" })); await user.type(within(composer).getByRole("textbox", { name: "Recipient Member ID" }), GRACE_ID); await user.click(within(composer).getByRole("button", { name: "Add recipient ID" })); @@ -1497,9 +1573,9 @@ describe("Relay handoff workbench", () => { expect(member).toHaveBeenCalledWith(GRACE_ID); expect(workstreams).not.toHaveBeenCalled(); expect(workstream).not.toHaveBeenCalled(); - expect(within(dialog).getByText(/private checkout-local draft.*Git-tracked Relay/i)).toBeVisible(); + expect(within(dialog).getByText(/writes a Relay Markdown artifact.*working tree/i)).toBeVisible(); expect(within(dialog).getByText(/records branch, HEAD, clean or dirty state, and observation time/i)).toBeVisible(); - expect(within(dialog).getByText(/commit and push.*teammates can receive/i)).toBeVisible(); + expect(within(dialog).getByText(/commit and push.*share it/i)).toBeVisible(); expect(within(dialog).getByText(/does not create a commit or capture source-file or local-change contents/i)).toBeVisible(); expect(await within(dialog).findByText(/Acting as Ada Lovelace/i)).toBeVisible(); const repositoryAtPublication = within(dialog).getByText(/codex\/hub-ux,/i).closest("p"); diff --git a/packages/hub-web/src/app/settings-page.test.tsx b/packages/hub-web/src/app/settings-page.test.tsx new file mode 100644 index 00000000..11d093ec --- /dev/null +++ b/packages/hub-web/src/app/settings-page.test.tsx @@ -0,0 +1,101 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import { HubApiError, type HubApi } from "../api/client"; +import { HubApiProvider } from "../api/context"; +import { createFixtureApi } from "../dev/fixture-api"; +import { AppRoutes } from "./App"; + +function renderSettings(api: HubApi) { + const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); + return { client, ...render( + + ) }; +} + +describe("Checkout logging settings", () => { + it("loads the quiet default without writing and exposes the project-notes destination", async () => { + const api = createFixtureApi(); + const save = vi.spyOn(api, "setLoggingPolicy"); + renderSettings(api); + expect(await screen.findByRole("radio", { name: "Significant events" })).toBeChecked(); + expect(screen.getAllByRole("radio")).toHaveLength(3); + expect(screen.getByRole("button", { name: "Save preference" })).toBeDisabled(); + expect(screen.getByRole("link", { name: "Settings" })).toHaveAttribute("aria-current", "page"); + expect(screen.getByRole("link", { name: /Read project notes/ })).toHaveAttribute("href", "/activity?source=legacy"); + expect(save).not.toHaveBeenCalled(); + }); + + it("saves only on explicit action and uses the returned revision for the next edit", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const save = vi.spyOn(api, "setLoggingPolicy"); + renderSettings(api); + await user.click(await screen.findByRole("radio", { name: "Only when asked" })); + expect(save).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Save preference" })); + const notice = await screen.findByText("Logging preference saved for this checkout."); + expect(save).toHaveBeenLastCalledWith({ mode: "manual", expectedRevision: null }); + await waitFor(() => expect(notice).toHaveFocus()); + const saved = await api.getLoggingPolicy(); + await user.click(screen.getByRole("radio", { name: "Task checkpoints" })); + await user.click(screen.getByRole("button", { name: "Save preference" })); + await waitFor(() => expect(save).toHaveBeenLastCalledWith({ mode: "checkpoints", expectedRevision: saved.revision })); + expect((await api.getLoggingPolicy()).mode).toBe("checkpoints"); + }); + + it("requires a reload after a concurrent preference change without overwriting it", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const save = vi.spyOn(api, "setLoggingPolicy").mockRejectedValueOnce(new HubApiError({ + type: "about:blank", status: 409, code: "REVISION_CONFLICT", title: "Revision conflict", detail: "Changed elsewhere.", + requestId: "settings-test", + })); + renderSettings(api); + await user.click(await screen.findByRole("radio", { name: "Only when asked" })); + await user.click(screen.getByRole("button", { name: "Save preference" })); + expect(await screen.findByRole("alert")).toHaveTextContent("changed in another session"); + expect(screen.getByRole("button", { name: "Save preference" })).toBeDisabled(); + await api.setLoggingPolicy({ mode: "checkpoints", expectedRevision: null }); + await user.click(screen.getByRole("button", { name: "Reload preference" })); + await waitFor(() => expect(screen.getByRole("radio", { name: "Task checkpoints" })).toBeChecked()); + expect(save).toHaveBeenCalledTimes(2); + expect(screen.queryByText("Logging preference saved for this checkout.")).not.toBeInTheDocument(); + }); + + it("cancels unsaved edits and never presents unreadable preferences as the default", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const save = vi.spyOn(api, "setLoggingPolicy"); + const view = renderSettings(api); + await user.click(await screen.findByRole("radio", { name: "Task checkpoints" })); + await user.click(screen.getByRole("button", { name: "Cancel" })); + expect(screen.getByRole("radio", { name: "Significant events" })).toBeChecked(); + expect(save).not.toHaveBeenCalled(); + view.unmount(); + vi.spyOn(api, "getLoggingPolicy").mockRejectedValue(new Error("Unreadable preference")); + renderSettings(api); + expect(await screen.findByText("This view could not be loaded")).toBeVisible(); + expect(screen.queryByRole("radio")).not.toBeInTheDocument(); + }); + + it("does not let an older in-flight read replace a confirmed saved preference", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const { client } = renderSettings(api); + await screen.findByRole("radio", { name: "Significant events" }); + let release!: (value: Awaited>) => void; + const old = new Promise>>((resolve) => { release = resolve; }); + vi.spyOn(api, "getLoggingPolicy").mockReturnValueOnce(old); + const read = client.refetchQueries({ queryKey: ["settings", "logging"] }); + await user.click(screen.getByRole("radio", { name: "Only when asked" })); + await user.click(screen.getByRole("button", { name: "Save preference" })); + await screen.findByText("Logging preference saved for this checkout."); + release({ mode: "significant", source: "default", revision: null }); + await read; + await waitFor(() => expect(client.getQueryData(["settings", "logging"])).toMatchObject({ mode: "manual", source: "local" })); + expect(screen.getByRole("radio", { name: "Only when asked" })).toBeChecked(); + }); +}); diff --git a/packages/hub-web/src/app/team-identity-activity.test.tsx b/packages/hub-web/src/app/team-identity-activity.test.tsx index 9898de5d..8f07db21 100644 --- a/packages/hub-web/src/app/team-identity-activity.test.tsx +++ b/packages/hub-web/src/app/team-identity-activity.test.tsx @@ -106,6 +106,45 @@ describe("Members identity and team directory", () => { expect(apply).not.toHaveBeenCalled(); }); + it("reactivates the existing inactive Member only after exact review and returns to their active record", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const original = await api.getMember(LIN_ID); + expect(original.active).toBe(false); + const preview = vi.spyOn(api, "previewTeamOperation"); + const apply = vi.spyOn(api, "applyTeamOperation"); + renderRoute(api, `/members?status=inactive&member=${LIN_ID}`); + await user.click(await screen.findByRole("button", { name: "Reactivate Member" })); + const dialog = await screen.findByRole("alertdialog", { name: `Reactivate ${original.displayName}?` }); + const confirm = within(dialog).getByRole("button", { name: "Reactivate Member" }); + await waitFor(() => expect(confirm).toBeEnabled()); + expect(apply).not.toHaveBeenCalled(); + expect(preview.mock.calls[0]![0]).toMatchObject({ action: { kind: "member.reactivate", memberId: LIN_ID }, expectedRevisions: [{ target: { kind: "artifact", path: original.sourcePath }, revision: original.revision }] }); + expect(within(dialog).getByText(/Commit and push to share it through Git/)).toBeVisible(); + await user.click(confirm); + await waitFor(() => expect(apply).toHaveBeenCalledTimes(1)); + expect(apply.mock.calls[0]![0]).toBe(await preview.mock.results[0]!.value); + await waitFor(() => expect(routeLocation()).toContain(`status=active&member=${LIN_ID}`)); + expect(await screen.findByText("Member reactivated")).toBeVisible(); + expect(await api.getMember(LIN_ID)).toMatchObject({ id: LIN_ID, active: true, displayName: original.displayName }); + }); + + it("keeps Member reactivation blocked when the reviewed target differs", async () => { + const user = userEvent.setup(); + const api = createFixtureApi(); + const realPreview = api.previewTeamOperation.bind(api); + vi.spyOn(api, "previewTeamOperation").mockImplementation(async (request) => ({ + ...await realPreview(request), request: { ...request, action: { kind: "member.reactivate", memberId: GRACE_ID } }, + })); + const apply = vi.spyOn(api, "applyTeamOperation"); + renderRoute(api, `/members?status=inactive&member=${LIN_ID}`); + await user.click(await screen.findByRole("button", { name: "Reactivate Member" })); + const dialog = await screen.findByRole("alertdialog"); + expect(await within(dialog).findByText("This view could not be loaded")).toBeVisible(); + expect(within(dialog).getByRole("button", { name: "Reactivate Member" })).toBeDisabled(); + expect(apply).not.toHaveBeenCalled(); + }); + it("marks a Git-alias match as You even without a local selection", async () => { const api = createFixtureApi({ memberFixture: "git-alias" }); renderRoute(api); diff --git a/packages/hub-web/src/components/ContextGraphCanvas.tsx b/packages/hub-web/src/components/ContextGraphCanvas.tsx new file mode 100644 index 00000000..489a9d7d --- /dev/null +++ b/packages/hub-web/src/components/ContextGraphCanvas.tsx @@ -0,0 +1,111 @@ +import { useEffect, useId, useMemo, useRef, useState, type CSSProperties } from "react"; +import { Maximize2, Minus, Plus } from "lucide-react"; +import { CONTEXT_EDGE_LIMIT, CONTEXT_NODE_LIMIT, contextCurve, layoutCodeSatellites, layoutContextGraph, type ContextEdge, type ContextNode } from "../lib/context-graph"; +import styles from "../styles/context-graph.module.css"; + +export interface ContextCodeNode { id: string; title: string; health: string } +interface Props { + nodes: readonly ContextNode[]; + edges: readonly ContextEdge[]; + selectedId: string | null; + selectedCodeId: string | null; + codeNodes: readonly ContextCodeNode[]; + highlightedIds?: ReadonlySet; + onSelect(id: string): void; + onSelectCode(id: string): void; + onClear(): void; +} + +const kinds = ["architecture", "component", "decision", "convention", "pattern", "guide", "risk", "fact", "task", "topic", "spec", "requirement", "constraint", "acceptance_criterion"]; +export function contextTypeColor(kind: string): string { return `var(--context-type-${Math.max(0, kinds.indexOf(kind)) % 6})`; } + +export function ContextGraphCanvas({ nodes, edges, selectedId, selectedCodeId, codeNodes, highlightedIds, onSelect, onSelectCode, onClear }: Props) { + const root = useRef(null); + const id = useId().replace(/:/g, ""); + const [size, setSize] = useState({ width: 760, height: 620 }); + const [view, setView] = useState({ x: 0, y: 0, scale: 1 }); + const drag = useRef<{ x: number; y: number; startX: number; startY: number; moved: boolean } | null>(null); + const boundedNodes = useMemo(() => nodes.slice(0, CONTEXT_NODE_LIMIT), [nodes]); + const world = useMemo(() => ({ width: Math.max(size.width, Math.ceil(Math.sqrt(boundedNodes.length * 22000))), height: Math.max(size.height, Math.ceil(boundedNodes.length / 5) * 110 + 60) }), [size, boundedNodes.length]); + const layout = useMemo(() => layoutContextGraph(boundedNodes, edges, world.width, world.height), [boundedNodes, edges, world]); + const satellites = useMemo(() => layoutCodeSatellites(codeNodes.map((node) => node.id), selectedId ? layout.get(selectedId) : undefined, layout, world.width, world.height), [codeNodes, selectedId, layout, world]); + const worldHeight = Math.max(world.height, ...[...satellites.values()].map((point) => point.y + 55)); + const links = useMemo(() => edges.slice(0, CONTEXT_EDGE_LIMIT).filter((edge) => layout.has(edge.source) && layout.has(edge.target)), [edges, layout]); + const neighbors = new Set(links.filter((edge) => edge.source === selectedId || edge.target === selectedId).flatMap((edge) => [edge.source, edge.target])); + + function fit() { + const scale = Math.min(1, size.width / world.width, size.height / worldHeight); + setView({ x: (size.width - world.width * scale) / 2, y: (size.height - worldHeight * scale) / 2, scale }); + } + function zoom(factor: number) { + setView((current) => { + const floor = Math.min(.35, size.width / world.width, size.height / worldHeight); + const scale = Math.max(floor, Math.min(2.5, current.scale * factor)); + const ratio = scale / current.scale; + return { scale, x: size.width / 2 - (size.width / 2 - current.x) * ratio, y: size.height / 2 - (size.height / 2 - current.y) * ratio }; + }); + } + useEffect(() => { + const element = root.current; + if (!element || typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver(([entry]) => { + if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) setSize({ width: entry.contentRect.width, height: entry.contentRect.height }); + }); + observer.observe(element); + return () => observer.disconnect(); + }, []); + useEffect(() => { + const scale = Math.min(1, size.width / world.width, size.height / world.height); + setView({ x: (size.width - world.width * scale) / 2, y: (size.height - world.height * scale) / 2, scale }); + }, [size, world]); + useEffect(() => { + const element = root.current; + if (!element) return; + const wheel = (event: WheelEvent) => { + if (!event.ctrlKey && !event.metaKey) return; + event.preventDefault(); zoom(event.deltaY > 0 ? .9 : 1.1); + }; + element.addEventListener("wheel", wheel, { passive: false }); + return () => element.removeEventListener("wheel", wheel); + }, [size, world.width, worldHeight]); + + return
    { if (event.key === "Escape") { onClear(); event.stopPropagation(); } }} + onPointerDown={(event) => { + if (event.button !== 0 || (event.target as Element).closest("button,a")) return; + event.currentTarget.setPointerCapture?.(event.pointerId); + drag.current = { x: event.clientX, y: event.clientY, startX: view.x, startY: view.y, moved: false }; + }} + onPointerMove={(event) => { + if (!drag.current) return; + const dx = event.clientX - drag.current.x, dy = event.clientY - drag.current.y; + if (Math.abs(dx) + Math.abs(dy) > 4) drag.current.moved = true; + const { startX, startY } = drag.current; + setView((current) => ({ ...current, x: startX + dx, y: startY + dy })); + }} + onPointerUp={() => { if (drag.current && !drag.current.moved) onClear(); drag.current = null; }} + onPointerCancel={() => { drag.current = null; }}> +
    + + {boundedNodes.map((node) => { + const point = layout.get(node.id)!; + const style = { left: point.x, top: point.y, "--node-color": contextTypeColor(node.kind) } as CSSProperties; + return ; + })} + {codeNodes.filter((node) => satellites.has(node.id)).map((node) => { + const point = satellites.get(node.id)!; + const parent = selectedId ? layout.get(selectedId) : undefined; + return ; + })} +
    +
    + + + +
    +

    Drag to pan · Ctrl / ⌘ scroll to zoom{codeNodes.length ? " · Fit graph to see every code node" : ""}

    +
    ; +} diff --git a/packages/hub-web/src/dev/fixture-api.ts b/packages/hub-web/src/dev/fixture-api.ts index ae5720f2..eb510d93 100644 --- a/packages/hub-web/src/dev/fixture-api.ts +++ b/packages/hub-web/src/dev/fixture-api.ts @@ -1,5 +1,8 @@ +import { isInboxCreate, isInboxUpdate } from "../lib/inbox-change"; import type { FixtureApiOptions, HubApi, JobSubscription } from "../api/client"; import type { + AgentLoggingPolicy, + AgentLoggingUpdateRequest, ActivityItem, ActivityRequest, ActivityResponse, @@ -60,6 +63,8 @@ import type { WikiBacklinksResponse, WikiEntityDetailResponse, WikiEntityListRequest, + WikiGraphResponse, + WikiGroundedCodeResponse, WikiEntityListResponse, WikiEntitySummary, WikiRelationsRequest, @@ -2033,6 +2038,20 @@ function fixtureRelaySummary(relay: RelayDetail): OverviewRelayItem { } class FixtureHubApi implements HubApi { + #loggingPolicy: AgentLoggingPolicy = { mode: "significant", source: "default", revision: null }; + #loggingRevision = 0; + + getLoggingPolicy(): Promise { + return Promise.resolve(structuredClone(this.#loggingPolicy)); + } + + async setLoggingPolicy(request: AgentLoggingUpdateRequest): Promise { + if (request.expectedRevision !== this.#loggingPolicy.revision) throw new Error("Logging preference changed. Reload and try again."); + this.#loggingPolicy = { + mode: request.mode, source: "local", revision: (++this.#loggingRevision).toString(16).padStart(64, "0"), + }; + return structuredClone(this.#loggingPolicy); + } readonly #jobs = structuredClone(jobs); readonly #members: TeamMember[]; readonly #workstreams = structuredClone(fixtureWorkstreams); @@ -2128,9 +2147,9 @@ class FixtureHubApi implements HubApi { )).length; const readyToTakeCount = actor.kind !== "member" ? null : this.#relays.filter((relay) => ( relay.state === "published" - && relay.recipients.some((recipient) => ( + && (relay.audience === "team" || relay.recipients.some((recipient) => ( recipient.kind === "member" && recipient.memberId === actor.memberId - )) + ))) )).length; const inYourHandsCount = actor.kind !== "member" ? null : this.#relays.filter((relay) => ( relay.state === "acknowledged" @@ -2295,9 +2314,9 @@ class FixtureHubApi implements HubApi { const readyRelayItems = currentMemberId === null ? [] : this.#relays .filter((relay) => ( relay.state === "published" - && relay.recipients.some((recipient) => ( + && (relay.audience === "team" || relay.recipients.some((recipient) => ( recipient.kind === "member" && recipient.memberId === currentMemberId - )) + ))) )) .slice(0, 3) .map(fixtureRelaySummary); @@ -2514,7 +2533,7 @@ class FixtureHubApi implements HubApi { return relay.sender.kind === "member" && relay.sender.memberId === current.memberId; } return relay.state === "published" - ? relay.recipients.some((recipient) => recipient.kind === "member" && recipient.memberId === current.memberId) + ? relay.audience === "team" || relay.recipients.some((recipient) => recipient.kind === "member" && recipient.memberId === current.memberId) : relay.acknowledgedBy?.kind === "member" && relay.acknowledgedBy.memberId === current.memberId; }).sort((left, right) => { if (left.publishedAt === null && right.publishedAt !== null) return 1; @@ -2670,6 +2689,7 @@ class FixtureHubApi implements HubApi { revision: envelope.preview.localChanges[0]?.afterRevision ?? revision("c"), updatedAt: envelope.receipt.authority.occurredAt, summary: action.draft.summary, + ...(action.draft.audience === undefined ? {} : { audience: action.draft.audience }), recipients: structuredClone(action.draft.recipients), input: structuredClone(action.draft), }; @@ -2685,13 +2705,14 @@ class FixtureHubApi implements HubApi { const id = envelope.receipt.purposeIds.find((item) => item.purpose === "relay")?.id; if (draft !== undefined && id !== undefined) { const next: RelayDetail = { - schemaVersion: 3, + schemaVersion: draft.input.audience === "team" ? 4 : 3, ref: { kind: "relay", id }, sourcePath: `.mex/relays/${id}.md`, revision: envelope.preview.changes[0]?.afterRevision ?? revision("d"), state: "published", sender: structuredClone(envelope.receipt.authority.actor), ...structuredClone(draft.input), + audience: draft.input.audience === "team" ? "team" : undefined, workstream: null, publishedAt: envelope.receipt.authority.occurredAt, publishedRepoState: structuredClone(envelope.receipt.authority.repoState), @@ -2868,6 +2889,15 @@ class FixtureHubApi implements HubApi { : "Remove the checkout-local Inbox draft.", }] : []; + const changedKnowledge = proposal?.change; + const knowledgePath = changedKnowledge?.kind === "knowledge.create" + ? changedKnowledge.entityKind === "pattern" + ? `.mex/patterns/${fixtureInboxCreatedSpecId}.md` + : `.mex/context/${changedKnowledge.entityKind}-${fixtureInboxCreatedSpecId}.md` + : changedKnowledge?.kind === "knowledge.update" + ? wikiEntities.find((entity) => entity.id === changedKnowledge.target.id)?.location.path + ?? `.mex/context/${changedKnowledge.target.kind}-${changedKnowledge.target.id}.md` + : undefined; const primaryChanges: InboxOperationPreviewResponse["preview"]["changes"] = action.kind === "inbox.draft.save" || action.kind === "inbox.draft.delete" ? [] : action.kind === "inbox.publish" @@ -2880,16 +2910,16 @@ class FixtureHubApi implements HubApi { }] : action.kind === "inbox.approve" ? [ - ...(proposal?.change.kind === "spec.create" ? [{ + ...(isInboxCreate(proposal?.change) ? [{ kind: "create" as const, - path: `.mex/specs/${fixtureInboxCreatedSpecId}.md`, - diff: `--- /dev/null\n+++ b/.mex/specs/${fixtureInboxCreatedSpecId}.md\n+title: ${proposal.title}\n`, + path: knowledgePath ?? `.mex/specs/${fixtureInboxCreatedSpecId}.md`, + diff: `--- /dev/null\n+++ b/${knowledgePath ?? `.mex/specs/${fixtureInboxCreatedSpecId}.md`}\n+title: ${proposal!.title}\n`, beforeRevision: null, afterRevision: revision("8"), }] : [{ kind: "update" as const, - path: `.mex/specs/${fixtureInboxSpecId}.md`, - diff: `--- a/.mex/specs/${fixtureInboxSpecId}.md\n+++ b/.mex/specs/${fixtureInboxSpecId}.md\n+The release gate records exact reviewed evidence.\n`, + path: knowledgePath ?? `.mex/specs/${fixtureInboxSpecId}.md`, + diff: `--- a/${knowledgePath ?? `.mex/specs/${fixtureInboxSpecId}.md`}\n+++ b/${knowledgePath ?? `.mex/specs/${fixtureInboxSpecId}.md`}\n+The release gate records exact reviewed evidence.\n`, beforeRevision: revision("3"), afterRevision: revision("8"), }]), { @@ -2933,7 +2963,7 @@ class FixtureHubApi implements HubApi { : action.kind === "inbox.publish" ? [{ purpose: "activity", id: eventId }, { purpose: "proposal", id: publishedProposalId }] : action.kind === "inbox.approve" - ? proposal?.change.kind === "spec.create" + ? isInboxCreate(proposal?.change) ? [{ purpose: "activity", id: eventId }, { purpose: "spec-entity", id: fixtureInboxCreatedSpecId }] : [{ purpose: "activity", id: eventId }] : [{ purpose: "activity", id: eventId }]; @@ -2980,7 +3010,7 @@ class FixtureHubApi implements HubApi { if (action.kind === "inbox.draft.save") { const id = action.draftId ?? envelope.receipt.purposeIds.find((item) => item.purpose === "inbox-draft")?.id; if (id !== undefined) { - const descriptor = action.draft.change.kind === "spec.create" + const descriptor = isInboxCreate(action.draft.change) ? { entityKind: action.draft.change.entityKind, title: action.draft.change.title } : { entityKind: action.draft.change.target.kind, title: action.draft.change.target.title ?? action.draft.change.target.id }; const next: InboxDraftDetail = { @@ -3033,7 +3063,7 @@ class FixtureHubApi implements HubApi { ))?.afterRevision ?? revision("9"); let updated: InboxProposalDetail; if (action.kind === "inbox.repair") { - const descriptor = action.replacement.change.kind === "spec.create" + const descriptor = isInboxCreate(action.replacement.change) ? { entityKind: action.replacement.change.entityKind, title: action.replacement.change.title, @@ -3095,7 +3125,7 @@ class FixtureHubApi implements HubApi { : [{ kind: "entity" as const, entity: { id: proposalId, kind: "proposal" } }]; const approvalTarget = action.kind !== "inbox.approve" ? [] - : proposals[0]?.change.kind === "spec.create" + : isInboxCreate(proposals[0]?.change) ? [{ kind: "entity" as const, entity: { @@ -3104,7 +3134,7 @@ class FixtureHubApi implements HubApi { kind: proposals[0].change.entityKind, }, }] - : proposals[0]?.change.kind === "spec.update" + : isInboxUpdate(proposals[0]?.change) ? [{ kind: "entity" as const, entity: structuredClone(proposals[0].change.target) }] : []; if (actionName !== null) { @@ -3143,6 +3173,7 @@ class FixtureHubApi implements HubApi { ? fixtureOperationId("member", sequence) : request.action.kind === "member.update" || request.action.kind === "member.deactivate" + || request.action.kind === "member.reactivate" || request.action.kind === "member.select" ? request.action.memberId : null; @@ -3160,6 +3191,7 @@ class FixtureHubApi implements HubApi { const canonical = request.action.kind === "member.add" || request.action.kind === "member.update" || request.action.kind === "member.deactivate" + || request.action.kind === "member.reactivate" || request.action.kind === "workstream.create" || request.action.kind === "workstream.update" || request.action.kind === "workstream.archive" @@ -3171,6 +3203,7 @@ class FixtureHubApi implements HubApi { const afterRevision = request.action.kind === "member.add" || request.action.kind === "workstream.create" ? revision("e") : request.action.kind === "member.update" || request.action.kind === "member.deactivate" + || request.action.kind === "member.reactivate" || request.action.kind === "workstream.update" || request.action.kind === "workstream.archive" ? revision("f") : revision("9"); @@ -3262,13 +3295,13 @@ class FixtureHubApi implements HubApi { revision: envelope.preview.changes[0]?.afterRevision ?? revision("e"), }; this.#members.push(member); - } else if (action.kind === "member.update" || action.kind === "member.deactivate") { + } else if (action.kind === "member.update" || action.kind === "member.deactivate" || action.kind === "member.reactivate") { const index = this.#members.findIndex((candidate) => candidate.id === action.memberId); const current = this.#members[index]; if (current !== undefined) { member = { ...current, - ...(action.kind === "member.update" ? action.patch : { active: false }), + ...(action.kind === "member.update" ? action.patch : { active: action.kind === "member.reactivate" }), revision: envelope.preview.changes[0]?.afterRevision ?? revision("f"), }; this.#members[index] = member; @@ -3331,13 +3364,14 @@ class FixtureHubApi implements HubApi { if (eventPurpose?.purpose === "activity") { const direct = action.kind === "activity.record" ? action.activity : null; const eventMember = member ?? ( - action.kind === "member.update" || action.kind === "member.deactivate" + action.kind === "member.update" || action.kind === "member.deactivate" || action.kind === "member.reactivate" ? this.#members.find((candidate) => candidate.id === action.memberId) ?? null : null ); const eventAction = action.kind === "member.add" ? "member.added" : action.kind === "member.update" ? "member.updated" : action.kind === "member.deactivate" ? "member.deactivated" + : action.kind === "member.reactivate" ? "member.reactivated" : action.kind === "workstream.create" ? "workstream.created" : action.kind === "workstream.update" ? "workstream.updated" : action.kind === "workstream.archive" ? "workstream.archived" @@ -3456,6 +3490,20 @@ class FixtureHubApi implements HubApi { return Promise.reject(error); } } + wikiGraph(): Promise { + const relations = wikiEntities.flatMap((entity) => [...wikiRelations(entity.id).items.map((hit) => hit.relation), ...wikiBacklinks(entity.id).items]); + return Promise.resolve({ indexedRevision: wikiRevision, observedAt: timestamp(0), nodes: wikiEntities, + relations: [...new Map(relations.map((edge) => [`${edge.source.id}:${edge.target.id}:${edge.type}`, edge])).values()], + coverage: { nodeLimit: 100, relationLimit: 500, nodesTruncated: false, relationsTruncated: false } }); + } + getWikiGroundedCode(id: string): Promise { + const detail = wikiDetail(id); + return Promise.resolve({ indexedRevision: wikiRevision, observedAt: timestamp(0), entityId: id, graphRevision, + groundings: detail.groundings.items.flatMap((grounding) => grounding.requestedNode ? [{ + requestedNode: grounding.requestedNode, resolvedNode: grounding.resolvedNode, health: grounding.health, + symbol: graphSymbols.find((symbol) => symbol.id === grounding.resolvedNode) ?? null, + }] : []), truncated: detail.groundings.truncated }); + } listWikiEntities(request: WikiEntityListRequest): Promise { const filtered = wikiEntities.filter((entity) => ( (!request.kind || entity.kind === request.kind) diff --git a/packages/hub-web/src/lib/activity-presentation.ts b/packages/hub-web/src/lib/activity-presentation.ts index f077ad32..66f12f66 100644 --- a/packages/hub-web/src/lib/activity-presentation.ts +++ b/packages/hub-web/src/lib/activity-presentation.ts @@ -35,6 +35,7 @@ const WORKFLOW_NARRATION: Readonly> = { "member.add": { action: "member.added", headline: "Added a teammate" }, "member.update": { action: "member.updated", headline: "Updated a teammate" }, "member.deactivate": { action: "member.deactivated", headline: "Deactivated a teammate" }, + "member.reactivate": { action: "member.reactivated", headline: "Reactivated a teammate" }, "workstream.create": { action: "workstream.created", headline: "Created a Workstream" }, "workstream.update": { action: "workstream.updated", headline: "Updated a Workstream" }, "workstream.archive": { action: "workstream.archived", headline: "Archived a Workstream" }, diff --git a/packages/hub-web/src/lib/context-graph.test.ts b/packages/hub-web/src/lib/context-graph.test.ts new file mode 100644 index 00000000..a8c99591 --- /dev/null +++ b/packages/hub-web/src/lib/context-graph.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; +import { + CONTEXT_NODE_LIMIT, + layoutCodeSatellites, + layoutContextGraph, + type ContextEdge, + type ContextNode, + type GraphPoint, +} from "./context-graph"; + +const types = ["architecture", "component", "convention", "decision", "guide", "pattern"]; +const nodes: readonly ContextNode[] = Array.from({ length: 30 }, (_, index) => ({ + id: `knowledge-${String(index).padStart(2, "0")}`, + title: `Repository knowledge ${index + 1}`, + kind: types[index % types.length]!, +})); +// Thirteen connected records and seventeen unlinked records, including sections. +const edges: readonly ContextEdge[] = Array.from({ length: 36 }, (_, index) => ({ + source: nodes[index % 13]!.id, + target: nodes[(index % 13 + 3 + Math.floor(index / 13)) % 13]!.id, + type: "related_to", +})); + +function expectFinite(point: GraphPoint): void { + expect(Number.isFinite(point.x)).toBe(true); + expect(Number.isFinite(point.y)).toBe(true); +} + +describe("Context knowledge layout", () => { + it("keeps the same positions when browse results arrive in a different order", () => { + const original = layoutContextGraph(nodes, edges, 960, 720); + const reordered = layoutContextGraph([...nodes].reverse(), edges, 960, 720); + + expect(reordered).toEqual(original); + }); + + it("keeps the same positions when recorded relationships arrive in a different order", () => { + const original = layoutContextGraph(nodes, edges, 960, 720); + const reordered = layoutContextGraph(nodes, [...edges].reverse(), 960, 720); + + expect(reordered).toEqual(original); + }); + + it.each([ + [720, 720], + [960, 720], + ])("includes all thirty records within a %i by %i canvas", (width, height) => { + const result = layoutContextGraph(nodes, edges, width, height); + + expect(result.size).toBe(30); + for (const node of nodes) { + const point = result.get(node.id); + expect(point).toBeDefined(); + expectFinite(point!); + expect(point!.x).toBeGreaterThanOrEqual(60); + expect(point!.x).toBeLessThanOrEqual(width - 60); + expect(point!.y).toBeGreaterThanOrEqual(30); + expect(point!.y).toBeLessThanOrEqual(height - 30); + } + expect(new Set([...result.values()].map(({ x, y }) => `${x},${y}`)).size).toBe(30); + for (const unlinked of nodes.slice(13)) expect(result.has(unlinked.id)).toBe(true); + }); + + it("handles empty knowledge and bounds a larger corpus", () => { + expect(layoutContextGraph([], [], 960, 720).size).toBe(0); + const largeCorpus = Array.from({ length: CONTEXT_NODE_LIMIT + 10 }, (_, index) => ({ + id: `large-${index}`, + title: `Knowledge ${index}`, + kind: "pattern", + })); + const result = layoutContextGraph(largeCorpus, [], 960, 720); + + expect(result.size).toBe(CONTEXT_NODE_LIMIT); + for (const point of result.values()) expectFinite(point); + }); + + it("ignores links whose endpoints cannot appear in the knowledge layout", () => { + const original = layoutContextGraph(nodes, edges, 960, 720); + const withUnavailableTargets = layoutContextGraph(nodes, [ + ...edges, + { source: nodes[0]!.id, target: "missing-record", type: "related_to" }, + { source: "missing-record", target: nodes[1]!.id, type: "related_to" }, + { source: nodes[0]!.id, target: nodes[0]!.id, type: "related_to" }, + ], 960, 720); + + expect(withUnavailableTargets).toEqual(original); + }); +}); + +describe("Context code expansion", () => { + it("places a small direct grounding set inside the canvas without moving knowledge", () => { + const base = layoutContextGraph(nodes, edges, 960, 720); + const before = structuredClone(base); + for (const point of base.values()) Object.freeze(point); + const parent = base.get(nodes[0]!.id); + const expanded = layoutCodeSatellites(["symbol-a", "symbol-b"], parent, base, 960, 720); + + expect([...expanded.keys()]).toEqual(["symbol-a", "symbol-b"]); + for (const point of expanded.values()) { + expectFinite(point); + expect(point.x).toBeGreaterThanOrEqual(74); + expect(point.x).toBeLessThanOrEqual(960 - 74); + expect(point.y).toBeGreaterThanOrEqual(36); + expect(point.y).toBeLessThanOrEqual(720 - 36); + for (const knowledge of base.values()) { + expect(Math.abs(point.x - knowledge.x) >= 132 || Math.abs(point.y - knowledge.y) >= 68).toBe(true); + } + } + expect(base).toEqual(before); + expect(layoutCodeSatellites(["symbol-a", "symbol-b"], parent, base, 960, 720)).toEqual(expanded); + layoutCodeSatellites(["another-symbol"], base.get(nodes[1]!.id), base, 960, 720); + expect(base).toEqual(before); + }); + + it("deduplicates and bounds a dense expansion without moving the base layout", () => { + const base = layoutContextGraph(nodes, edges, 720, 720); + const before = structuredClone(base); + const ids = Array.from({ length: 70 }, (_, index) => `symbol-${index}`); + const expanded = layoutCodeSatellites([ids[0]!, ...ids], base.get(nodes[0]!.id), base, 720, 720); + + expect(expanded.size).toBe(50); + for (const point of expanded.values()) { + expectFinite(point); + expect(point.x).toBeGreaterThanOrEqual(0); + expect(point.x).toBeLessThanOrEqual(720); + expect(point.y).toBeGreaterThanOrEqual(0); + // Dense code sets can extend the canvas; retained work must still be finite. + expect(point.y).toBeLessThanOrEqual(720 + 50 * 100); + } + expect(base).toEqual(before); + }); + + it("shows no satellites without a selected knowledge record or without groundings", () => { + const base = layoutContextGraph(nodes, edges, 960, 720); + + expect(layoutCodeSatellites(["symbol-a"], undefined, base, 960, 720).size).toBe(0); + expect(layoutCodeSatellites([], base.get(nodes[0]!.id), base, 960, 720).size).toBe(0); + }); +}); diff --git a/packages/hub-web/src/lib/context-graph.ts b/packages/hub-web/src/lib/context-graph.ts new file mode 100644 index 00000000..2f5ce331 --- /dev/null +++ b/packages/hub-web/src/lib/context-graph.ts @@ -0,0 +1,87 @@ +export interface ContextNode { id: string; title: string; kind: string } +export interface ContextEdge { source: string; target: string; type: string; note?: string | null } +export interface GraphPoint { x: number; y: number } +export const CONTEXT_NODE_LIMIT = 100; +export const CONTEXT_EDGE_LIMIT = 500; + +function hash(value: string): number { + let result = 2166136261; + for (const character of value) result = Math.imul(result ^ character.charCodeAt(0), 16777619); + return result >>> 0; +} + +/** A finite layout pass. Index order and selection never change the base positions. */ +export function layoutContextGraph(nodes: readonly ContextNode[], edges: readonly ContextEdge[], width: number, height: number): Map { + const entries = [...nodes.slice(0, CONTEXT_NODE_LIMIT)].sort((a, b) => a.id.localeCompare(b.id)); + const kinds = [...new Set(entries.map((node) => node.kind))].sort(); + const points = entries.map((node, index) => { + const angle = kinds.indexOf(node.kind) * Math.PI * 2 / Math.max(kinds.length, 1) - Math.PI / 2; + const spread = (hash(node.id) % 1000) / 1000; + return { + id: node.id, + x: width / 2 + Math.cos(angle) * width * .22 + Math.cos(index * 2.39996) * (40 + spread * width * .15), + y: height / 2 + Math.sin(angle) * height * .22 + Math.sin(index * 2.39996) * (40 + spread * height * .15), + }; + }); + const byId = new Map(points.map((point) => [point.id, point])); + const links = edges.slice(0, CONTEXT_EDGE_LIMIT).sort((a, b) => `${a.source}:${a.target}:${a.type}`.localeCompare(`${b.source}:${b.target}:${b.type}`)).flatMap((edge) => { + const a = byId.get(edge.source), b = byId.get(edge.target); + return a && b && a !== b ? [[a, b] as const] : []; + }); + for (let pass = 0; pass < 220; pass++) { + if (pass < 100) for (const [a, b] of links) { + const dx = b.x - a.x, dy = b.y - a.y; + if (Math.hypot(dx, dy) > 175) { + a.x += dx * .003; a.y += dy * .003; + b.x -= dx * .003; b.y -= dy * .003; + } + } + for (let i = 0; i < points.length; i++) for (let j = i + 1; j < points.length; j++) { + const a = points[i]!, b = points[j]!; + const dx = b.x - a.x, dy = b.y - a.y; + const overlapX = 124 - Math.abs(dx), overlapY = 72 - Math.abs(dy); + if (overlapX > 0 && overlapY > 0) { + if (overlapX / 124 < overlapY / 72) { + const push = overlapX * .51 * (dx >= 0 ? 1 : -1); + a.x -= push; b.x += push; + } else { + const push = overlapY * .51 * (dy >= 0 ? 1 : -1); + a.y -= push; b.y += push; + } + } + } + for (const point of points) { + point.x = Math.max(66, Math.min(width - 66, point.x)); + point.y = Math.max(36, Math.min(height - 45, point.y)); + } + } + return new Map(points.map(({ id, x, y }) => [id, { x, y }])); +} + +/** Satellites occupy free space; the underlying knowledge layout stays fixed. */ +export function layoutCodeSatellites(ids: readonly string[], parent: GraphPoint | undefined, base: ReadonlyMap, width: number, height: number): Map { + if (!parent) return new Map(); + const occupied = [...base.values()].map((point) => ({ ...point, width: 116, height: 64 })); + const result = new Map(); + for (const [index, id] of [...new Set(ids)].slice(0, 50).entries()) { + let chosen: GraphPoint | undefined; + for (let radius = 90; radius <= Math.max(width, height) && !chosen; radius += 18) { + for (let step = 0; step < 32; step++) { + const angle = step * Math.PI / 16 + index * .15; + const point = { x: parent.x + Math.cos(angle) * radius, y: parent.y + Math.sin(angle) * radius }; + if (point.x < 78 || point.x > width - 78 || point.y < 40 || point.y > height - 50) continue; + if (!occupied.some((other) => Math.abs(point.x - other.x) < (148 + other.width) / 2 + 5 && Math.abs(point.y - other.y) < (72 + other.height) / 2 + 5)) { + chosen = point; break; + } + } + } + chosen ??= { x: 90 + (index % Math.max(1, Math.floor(width / 180))) * 180, y: height + 55 + Math.floor(index / Math.max(1, Math.floor(width / 180))) * 90 }; + result.set(id, chosen); + occupied.push({ ...chosen, width: 148, height: 72 }); + } + return result; +} + +export function contextCurve(a: GraphPoint, b: GraphPoint, bend = 16): string { + return `M ${a.x} ${a.y - 15} Q ${(a.x + b.x) / 2 + bend} ${(a.y + b.y) / 2 - 15 - bend} ${b.x} ${b.y - 15}`; +} diff --git a/packages/hub-web/src/lib/inbox-change.ts b/packages/hub-web/src/lib/inbox-change.ts new file mode 100644 index 00000000..80340a48 --- /dev/null +++ b/packages/hub-web/src/lib/inbox-change.ts @@ -0,0 +1,20 @@ +import type { InboxDraftInput, InboxKnowledgeKind } from "../api/types"; + +export const inboxKnowledgeKinds: readonly InboxKnowledgeKind[] = [ + "architecture", "component", "convention", "decision", "pattern", "guide", +]; + +export type InboxCreateChange = Extract; +export type InboxUpdateChange = Extract; + +export function isInboxCreate(change: InboxDraftInput["change"] | undefined): change is InboxCreateChange { + return change?.kind === "spec.create" || change?.kind === "knowledge.create"; +} + +export function isInboxUpdate(change: InboxDraftInput["change"] | undefined): change is InboxUpdateChange { + return change?.kind === "spec.update" || change?.kind === "knowledge.update"; +} + +export function isInboxKnowledgeKind(kind: string): kind is InboxKnowledgeKind { + return inboxKnowledgeKinds.some((candidate) => candidate === kind); +} diff --git a/packages/hub-web/src/pages/ContextPage.tsx b/packages/hub-web/src/pages/ContextPage.tsx new file mode 100644 index 00000000..cb70ebf0 --- /dev/null +++ b/packages/hub-web/src/pages/ContextPage.tsx @@ -0,0 +1,124 @@ +import { lazy, Suspense, useEffect, useMemo, useState, type CSSProperties } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Link, useOutletContext, useSearchParams } from "react-router-dom"; +import { ArrowUpRight, Network, X } from "lucide-react"; +import { useHubApi } from "../api/context"; +import { HubApiError } from "../api/client"; +import type { CapabilitiesResponse } from "../api/types"; +import { PageHeader, sentenceCase, StatePanel, StatusPill } from "../components/ui"; +import { Button, buttonVariants } from "../components/primitives/button"; +import { Input } from "../components/primitives/input"; +import { ContextGraphCanvas, contextTypeColor } from "../components/ContextGraphCanvas"; +import styles from "../styles/context.module.css"; + +const KnowledgeList = lazy(async () => ({ default: (await import("./KnowledgePage")).KnowledgePage })); +const emptyNodes: never[] = []; + +function ReadProblem({ error, retry }: { error: unknown; retry(): void }) { + const problem = error instanceof HubApiError ? error.problem : null; + return Open Health} />; +} + +export function ContextPage() { + const [params, setParams] = useSearchParams(); + // Existing saved list filters remain usable when opening an older link. + const list = params.get("view") === "list" || (!params.has("view") && ["q", "kind", "topic", "lifecycle", "grounding", "sourceType"].some((key) => params.has(key))); + return
    +
    +
    + + +
    +
    + {list ? }> : } +
    ; +} + +function ContextExplorer() { + const api = useHubApi(); + const { capabilities } = useOutletContext<{ capabilities?: CapabilitiesResponse }>(); + const enabled = capabilities?.wiki.read.availability === "available"; + const graph = useQuery({ queryKey: ["wiki-graph"], queryFn: () => api.wikiGraph(), enabled, retry: false }); + const [selectedId, setSelectedId] = useState(null); + const [codeId, setCodeId] = useState(null); + const [codeLimit, setCodeLimit] = useState(12); + const [query, setQuery] = useState(""); + const [kind, setKind] = useState(null); + const revision = graph.data?.indexedRevision; + useEffect(() => { setSelectedId(null); setCodeId(null); }, [revision]); + const nodes = graph.data?.nodes ?? emptyNodes; + const selected = nodes.find((node) => node.id === selectedId); + const detail = useQuery({ queryKey: ["context-detail", selectedId, revision], queryFn: () => api.getWikiEntity(selectedId!), enabled: enabled && !!selected, retry: false }); + const code = useQuery({ queryKey: ["context-code", selectedId, revision], queryFn: () => api.getWikiGroundedCode(selectedId!), enabled: enabled && !!selected, retry: false }); + const conflict = !!selected && (!!detail.data && (detail.data.indexedRevision !== revision || detail.data.entity.id !== selectedId) + || !!code.data && (code.data.indexedRevision !== revision || code.data.entityId !== selectedId)); + const record = !conflict && !detail.error ? detail.data : undefined; + const bridge = !conflict && !code.error ? code.data : undefined; + const symbols = useMemo(() => [...new Map((bridge?.groundings ?? []).flatMap((item) => item.symbol ? [[item.symbol.id, { symbol: item.symbol, health: item.health }] as const] : [])).values()], [bridge]); + const codeNodes = useMemo(() => symbols.slice(0, codeLimit).map(({ symbol, health }) => ({ id: symbol.id, title: symbol.name, health })), [symbols, codeLimit]); + const edges = useMemo(() => (graph.data?.relations ?? []).map((edge) => ({ source: edge.source.id, target: edge.target.id, type: edge.type, note: edge.note })), [graph.data]); + const kinds = useMemo(() => [...new Set(nodes.map((node) => node.kind))].sort(), [nodes]); + const highlighted = useMemo(() => query.trim() || kind ? new Set(nodes.filter((node) => (!kind || node.kind === kind) && `${node.title} ${node.summary ?? ""} ${node.location.path}`.toLowerCase().includes(query.trim().toLowerCase())).map((node) => node.id)) : undefined, [nodes, query, kind]); + const activeCode = symbols.find(({ symbol }) => symbol.id === codeId); + const relations = (graph.data?.relations ?? []).filter((edge) => edge.source.id === selectedId || edge.target.id === selectedId); + function select(id: string) { setSelectedId(id); setCodeId(null); setCodeLimit(12); } + function clear() { setSelectedId(null); setCodeId(null); } + function reload() { clear(); void graph.refetch(); } + + if (!capabilities) return ; + if (!enabled) return ; + if (graph.error) return ; + if (!graph.data) return ; + if (!nodes.length) return ; + + return
    { if (event.key === "Escape") clear(); }}> +
    + +
    + + {kinds.map((type) => )} +
    + {highlighted ? `${highlighted.size} matching · ` : ""}{nodes.length} entities · {edges.length} links +
    + {graph.data.coverage.nodesTruncated || graph.data.coverage.relationsTruncated ?

    Partial graph: up to 100 entities and 500 relationships are shown. Use List to browse more knowledge.

    : null} +
    + + +
    +
    ; +} diff --git a/packages/hub-web/src/pages/HomeOverview.tsx b/packages/hub-web/src/pages/HomeOverview.tsx index c11e3003..91c433f6 100644 --- a/packages/hub-web/src/pages/HomeOverview.tsx +++ b/packages/hub-web/src/pages/HomeOverview.tsx @@ -10,8 +10,8 @@ import { CircleDashed, ExternalLink, GitBranch, - Inbox, LoaderCircle, + Network, RadioTower, RefreshCw, ScrollText, @@ -21,7 +21,7 @@ import { } from "lucide-react"; import { useState, type CSSProperties, type ReactNode } from "react"; import { Link } from "react-router-dom"; -import { InboxProposalIdSchema, RelayIdSchema } from "@mex/hub-contracts/ids"; +import { RelayIdSchema } from "@mex/hub-contracts/ids"; import { useHubApi } from "../api/context"; import type { ActivityItem, OverviewResponse } from "../api/types"; import { @@ -124,14 +124,20 @@ function HomeHeader({ title="Overview" description={data ? `Last checked ${formatDate(data.observedAt)}` : undefined} actions={( - +
    + + +
    )} /> ); @@ -194,15 +200,6 @@ function actorAttentionDescription(data: OverviewResponse): string { return "Review the team identity MEX uses for shared actions in this checkout."; } -function proposalRoute(source: FocusPanel["inbox"]): string { - if (source.availability !== "available") return "/inbox?view=review"; - const proposal = source.items[0]; - const parsed = proposal ? InboxProposalIdSchema.safeParse(proposal.ref.id) : null; - return parsed?.success - ? `/inbox?view=review&proposal=${encodeURIComponent(parsed.data)}` - : "/inbox?view=review"; -} - function relayRoute(source: FocusPanel["relays"], kind: "ready" | "claimed"): string { if (source.availability !== "available") return "/relays?view=mine&state=open"; const relay = kind === "ready" ? source.readyToTake[0] : source.inYourHands[0]; @@ -267,16 +264,6 @@ function buildFocusItems(data: OverviewResponse): FocusItemView[] { icon: UserRound, }); } - if (focus?.inbox.availability === "available" && focus.inbox.teamReviewCount > 0) { - const count = focus.inbox.teamReviewCount; - items.push({ - id: "inbox", - title: count === 1 ? "Review one proposed Spec change" : `Review ${count} proposed Spec changes`, - action: "Open Inbox", - route: proposalRoute(focus.inbox), - icon: Inbox, - }); - } if (focus?.relays.availability === "available" && focus.relays.readyToTakeCount > 0) { const count = focus.relays.readyToTakeCount; items.push({ @@ -335,9 +322,6 @@ function focusWarnings(data: OverviewResponse): Array<{ label: string; reason: s if (data.focus.identity.availability === "unavailable") { warnings.push({ label: "Identity focus", reason: data.focus.identity.reason }); } - if (data.focus.inbox.availability === "unavailable") { - warnings.push({ label: "Inbox focus", reason: data.focus.inbox.reason }); - } if (data.focus.relays.availability === "unavailable") { warnings.push({ label: "Relay focus", reason: data.focus.relays.reason }); } @@ -371,42 +355,6 @@ function FocusTechnicalDetails({ data }: { data: OverviewResponse }) { ))} ) :
    Identity source
    {data.identity.reason}
    } - {focus?.inbox.availability === "available" ? ( - <> -
    Inbox revision
    {focus.inbox.deterministicRevision}
    -
    Inbox source truncated
    {focus.inbox.sourceTruncated ? "Yes" : "No"}
    -
    Inbox diagnostics truncated
    {focus.inbox.diagnosticsTruncated ? "Yes" : "No"}
    - {focus.inbox.diagnostics.map((diagnostic, index) => ( -
    -
    {diagnostic.code}
    -
    {diagnostic.path ? {diagnostic.path} : diagnostic.message}
    -
    - ))} - - ) : null} - {focus?.inbox.availability === "unavailable" ? ( - <> -
    Inbox source
    {focus.inbox.reason}
    - {focus.inbox.deterministicRevision ? ( -
    Inbox revision
    {focus.inbox.deterministicRevision}
    - ) : null} - {focus.inbox.truncated !== undefined ? ( -
    Inbox corpus truncated
    {focus.inbox.truncated ? "Yes" : "No"}
    - ) : null} - {focus.inbox.sourceTruncated !== undefined ? ( -
    Inbox source truncated
    {focus.inbox.sourceTruncated ? "Yes" : "No"}
    - ) : null} - {focus.inbox.diagnosticsTruncated !== undefined ? ( -
    Inbox diagnostics truncated
    {focus.inbox.diagnosticsTruncated ? "Yes" : "No"}
    - ) : null} - {focus.inbox.diagnostics?.map((diagnostic, index) => ( -
    -
    {diagnostic.code}
    -
    {diagnostic.path ? {diagnostic.path} : diagnostic.message}
    -
    - ))} - - ) : null} {focus?.relays.availability === "available" ? ( <>
    Relay revision
    {focus.relays.deterministicRevision}
    @@ -458,6 +406,12 @@ function FocusCard({ data, onRetry }: { data: OverviewResponse; onRetry: () => v

    Attention

    + + +
    {primary && PrimaryIcon ? ( @@ -479,9 +433,9 @@ function FocusCard({ data, onRetry }: { data: OverviewResponse; onRetry: () => v You’re caught up - @@ -559,18 +513,7 @@ function ActivityRow({ item }: { item: ActivityItem }) { ); } -/** - * Where the signup lives, and why the Hub never sees the address. - * - * The form is hosted; this card only opens it. That is not a shortcut — the Hub - * serves itself under a policy that permits neither an outbound `fetch` nor a - * cross-origin form post (`src/hub/app.ts`, `connect-src 'self'`, - * `form-action 'self'`), so an input here could not submit anywhere without - * loosening the rule that makes "Runs locally" in the sidebar true. Opening a - * link is a navigation rather than a connection, so it stays inside the policy. - * - * The consequence worth stating: **no email address ever passes through mex.** - */ +/** Hosted voluntary feedback; no address or installation ID passes through the Hub. */ const UPDATES_FORM = "https://tally.so/r/KYjv4k"; /** @@ -584,12 +527,12 @@ function UpdatesSignupCard() {
    -

    Help make MEX better

    +

    Help shape MEX

    - A short form about how you’re using it. Your answers help shape mex :) + Tell us how you use MEX and what we should improve.

    {/* @@ -602,7 +545,7 @@ function UpdatesSignupCard() { size="sm" variant="outline" > - Open the form + Share feedback
    @@ -635,7 +578,7 @@ function LatestActivityCard({ activity, onRetry }: { activity: OverviewResponse[ No team memory yet - Shared MEX changes, including agent-prepared Spec proposals and handoffs, will appear here automatically. + Recorded team changes, handoffs, and project notes will appear here. ) : ( @@ -924,7 +867,8 @@ function OperationCard({ operation }: { operation: OverviewResponse["operation"] const job = operation.active ?? operation.latestRelevantFailure; if (job === null) return null; const isActive = operation.active !== null; - const percent = job.progress?.total === undefined + const graph = job.kind === "graph_refresh" || job.kind === "graph_rebuild"; + const percent = job.progress?.total === undefined || (graph && job.phase !== "parse") ? null : Math.round((job.progress.completed / job.progress.total) * 100); return ( @@ -952,7 +896,9 @@ function OperationCard({ operation }: { operation: OverviewResponse["operation"] {sentenceCase(job.kind)} · {sentenceCase(job.phase)} - {() => percent !== null + {() => graph && job.progress + ? `${job.progress.completed}${job.progress.total === undefined ? "" : ` / ${job.progress.total}`} files parsed` + : percent !== null ? `${job.progress!.completed} / ${job.progress!.total}` : job.progress ? `${job.progress.completed} completed` : "In progress"} diff --git a/packages/hub-web/src/pages/InboxMutationDialogs.tsx b/packages/hub-web/src/pages/InboxMutationDialogs.tsx index 9caa0121..77ac60d7 100644 --- a/packages/hub-web/src/pages/InboxMutationDialogs.tsx +++ b/packages/hub-web/src/pages/InboxMutationDialogs.tsx @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; import { AlertTriangle, CheckCircle2, @@ -29,6 +29,8 @@ import type { InboxOperationPreviewResponse, InboxProposalDetail, InboxSpecKind, + InboxEntityKind, + InboxKnowledgeKind, TeamActorRef, } from "../api/types"; import { @@ -64,6 +66,8 @@ import { NativeSelect, NativeSelectOption } from "../components/primitives/nativ import { Textarea } from "../components/primitives/textarea"; import { ErrorState, StatePanel, StatusPill, formatDate, sentenceCase } from "../components/ui"; import styles from "../styles/inbox-mutations.module.css"; +import { boundedNextCursor, MAX_WORKBENCH_PAGES } from "../lib/bounds"; +import { inboxKnowledgeKinds, isInboxCreate, isInboxUpdate, isInboxKnowledgeKind } from "../lib/inbox-change"; type CreateChange = Extract; type CreateRelation = NonNullable; @@ -166,10 +170,10 @@ function actorLabel(actor: TeamActorRef): string { function changeLabel( changeKind: InboxDraftDetail["changeKind"], - entityKind: InboxSpecKind, + entityKind: InboxEntityKind, ): string { const label = entityKind === "spec" ? "Spec" : entityKind.replaceAll("_", " "); - return `${changeKind === "spec.create" ? "New" : "Update"} ${label}`; + return `${changeKind.endsWith(".create") ? "New" : "Update"} ${label}`; } function hasLoneSurrogate(value: string): boolean { @@ -349,6 +353,44 @@ function ExactPreviewDetails({ envelope }: { envelope: InboxOperationPreviewResp ); } +function KnowledgeTargetPicker({ kind, targetId, targetTitle, busy, onSelect }: { + kind: InboxKnowledgeKind; + targetId: string; + targetTitle: string; + busy: boolean; + onSelect(id: string): void; +}) { + const api = useHubApi(); + const records = useInfiniteQuery({ + queryKey: ["inbox", "knowledge-targets", kind], + queryFn: ({ pageParam }) => api.listWikiEntities({ kind, limit: 25, ...(pageParam ? { cursor: pageParam } : {}) }), + initialPageParam: undefined as string | undefined, + getNextPageParam: (page, pages) => boundedNextCursor(page.nextCursor, pages.length), + retry: false, + }); + const rows = records.data?.pages.flatMap((page) => page.items) ?? []; + return ( + + Knowledge to update + onSelect(event.currentTarget.value)}> + {records.isPending ? "Loading knowledge…" : "Choose a record"} + {targetId && !rows.some((row) => row.id === targetId) + ? {targetTitle || targetId} : null} + {rows.map((row) => {row.title})} + + {records.isError ? Knowledge could not be loaded. : null} + {records.hasNextPage ? : null} + {(records.data?.pages.length ?? 0) >= MAX_WORKBENCH_PAGES && records.data?.pages.at(-1)?.nextCursor + ? This picker reached its page limit. Open Context to find the record. : null} + {targetId ? + Open current record + {" · "} + : Select a record to load its current wording.} + + ); +} + function DraftEditorDialog({ draft, repair, @@ -380,6 +422,8 @@ function DraftEditorDialog({ targetRevisions: repair.targetRevisions, }); const existingChange = sourceInput?.change; + const legacy = existingChange?.kind.startsWith("spec.") ?? false; + const subject = legacy ? "Spec" : "knowledge"; const existingExpectations = new Map( (sourceInput?.targetRevisions ?? []).map((item) => [item.target.id, item]), ); @@ -390,46 +434,46 @@ function DraftEditorDialog({ ? undefined : existingExpectations.get(existingRelation.target.id); const [mode, setMode] = useState<"create" | "update">( - existingChange?.kind === "spec.update" ? "update" : "create", + isInboxUpdate(existingChange) ? "update" : "create", ); const [includeTitle, setIncludeTitle] = useState( - existingChange?.kind === "spec.update" + isInboxUpdate(existingChange) ? Object.hasOwn(existingChange.patch, "title") : false, ); const [includeSummary, setIncludeSummary] = useState( - existingChange?.kind === "spec.update" + isInboxUpdate(existingChange) ? Object.hasOwn(existingChange.patch, "summary") : false, ); const [includeBody, setIncludeBody] = useState( - existingChange?.kind === "spec.update" + isInboxUpdate(existingChange) ? Object.hasOwn(existingChange.patch, "body") : true, ); - const [entityKind, setEntityKind] = useState( - existingChange?.kind === "spec.create" ? existingChange.entityKind : "requirement", + const [entityKind, setEntityKind] = useState( + isInboxCreate(existingChange) ? existingChange.entityKind : "pattern", ); const [title, setTitle] = useState( - existingChange?.kind === "spec.create" + isInboxCreate(existingChange) ? existingChange.title : existingChange?.patch.title ?? "", ); const [summary, setSummary] = useState( - existingChange?.kind === "spec.create" + isInboxCreate(existingChange) ? existingChange.summary ?? "" : existingChange?.patch.summary ?? "", ); const [body, setBody] = useState( - existingChange?.kind === "spec.create" + isInboxCreate(existingChange) ? existingChange.body : existingChange?.patch.body ?? "", ); const [status, setStatus] = useState<"in_flight" | "promoted">( - existingChange?.kind === "spec.create" ? existingChange.status : "in_flight", + isInboxCreate(existingChange) ? existingChange.status : "in_flight", ); const [topicsText, setTopicsText] = useState( - existingChange?.kind === "spec.create" + isInboxCreate(existingChange) ? (existingChange.topics ?? []).map((id) => { const expectation = existingExpectations.get(id); return expectation === undefined @@ -451,15 +495,15 @@ function DraftEditorDialog({ existingRelationExpectation ? String(existingRelationExpectation.semanticRevision) : "", ); const [targetId, setTargetId] = useState( - existingChange?.kind === "spec.update" ? existingChange.target.id : "", + isInboxUpdate(existingChange) ? existingChange.target.id : "", ); - const [targetKind, setTargetKind] = useState( - existingChange?.kind === "spec.update" ? existingChange.target.kind : "spec", + const [targetKind, setTargetKind] = useState( + isInboxUpdate(existingChange) ? existingChange.target.kind : "pattern", ); const [targetTitle, setTargetTitle] = useState( - existingChange?.kind === "spec.update" ? existingChange.target.title ?? "" : "", + isInboxUpdate(existingChange) ? existingChange.target.title ?? "" : "", ); - const existingTarget = existingChange?.kind === "spec.update" + const existingTarget = isInboxUpdate(existingChange) ? existingExpectations.get(existingChange.target.id) : undefined; const [targetRevision, setTargetRevision] = useState(existingTarget?.revision ?? ""); @@ -469,11 +513,37 @@ function DraftEditorDialog({ const [rationale, setRationale] = useState(sourceInput?.rationale ?? ""); const preservedEvidence = sourceInput?.evidence ?? []; const [evidenceNote, setEvidenceNote] = useState(""); + const [targetNotice, setTargetNotice] = useState(""); const [envelope, setEnvelope] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const confirmTriggerRef = useRef(null); const applySucceeded = useRef(false); const previewGeneration = useRef(0); + const targetGeneration = useRef(0); + const targetLookup = useMutation({ + mutationFn: ({ id }: { id: string; generation: number }) => api.getWikiEntity(id), + onSuccess: (detail, { id, generation }) => { + if (generation !== targetGeneration.current) return; + if (detail.entity.id !== id || !isInboxKnowledgeKind(detail.entity.kind) || detail.entity.version.semanticRevision < 1) { + setTargetNotice("This record is not available for a knowledge proposal."); + return; + } + setTargetId(id); + setTargetKind(detail.entity.kind); + setTargetTitle(detail.entity.title); + setTargetRevision(detail.entity.version.contentHash); + setSemanticRevision(String(detail.entity.version.semanticRevision)); + const bodyFits = !detail.body.truncated && canonicalText(detail.body.content, 16 * 1024, true); + if (!sourceInput) { + setTitle(detail.entity.title); + setSummary(detail.entity.summary ?? ""); + setBody(bodyFits ? detail.body.content : ""); + setIncludeBody(bodyFits); + } + setTargetNotice(bodyFits ? "Current record loaded. Only the selected fields will change." : "This body exceeds the editor limit. You can update its title or summary, or write a complete replacement body."); + invalidate(); + }, + }); const preview = useMutation({ mutationFn: ({ request }: { request: InboxOperationPreviewRequest; generation: number }) => ( api.previewInboxOperation(request) @@ -538,22 +608,33 @@ function DraftEditorDialog({ setter(value); invalidate(); }; + const selectKnowledgeTarget = (id: string) => { + targetGeneration.current += 1; + setTargetRevision(""); + setTargetNotice(""); + invalidate(); + if (!id) { + setTargetId(""); setTargetTitle(""); targetLookup.reset(); + return; + } + targetLookup.mutate({ id, generation: targetGeneration.current }); + }; const targetRevisionValid = /^[a-f0-9]{64}$/.test(targetRevision); const semanticRevisionNumber = Number(semanticRevision); const topicAttestations = parseTopicAttestations(topicsText); const relationSemanticRevisionNumber = Number(relationSemanticRevision); - const relationIsValid = relationType === "none" || ( + const relationIsValid = !legacy || relationType === "none" || ( /^mx_[0-7][0-9A-HJKMNP-TV-Z]{25}$/.test(relationTargetId) && /^[a-f0-9]{64}$/.test(relationRevision) && Number.isInteger(relationSemanticRevisionNumber) && relationSemanticRevisionNumber > 0 && canonicalText(relationTargetTitle, 512, false) - && relationAccepted(entityKind, relationType, relationTargetKind) + && legacy && relationAccepted(entityKind as InboxSpecKind, relationType, relationTargetKind) ); const createExpectationById = new Map(); let createExpectationConflict = false; for (const item of topicAttestations.items) createExpectationById.set(item.id, item); - if (relationType !== "none" && relationIsValid) { + if (legacy && relationType !== "none" && relationIsValid) { const current = createExpectationById.get(relationTargetId); if (current !== undefined && ( current.revision !== relationRevision @@ -576,7 +657,7 @@ function DraftEditorDialog({ semanticRevision: item.semanticRevision, })); const updateHasPatch = includeTitle || includeSummary || includeBody; - const canPreview = canonicalText(rationale, 8 * 1024, true) + const canPreview = !targetLookup.isPending && canonicalText(rationale, 8 * 1024, true) && (mode === "create" ? canonicalText(body, 16 * 1024, true) && canonicalText(title, 512, true) : (!includeBody || canonicalText(body, 16 * 1024, true)) @@ -596,53 +677,35 @@ function DraftEditorDialog({ && updateHasPatch )); - const draftInput = (): InboxDraftInput => ({ - change: mode === "create" - ? { - kind: "spec.create", - entityKind, - title, - body, - ...(summary === "" ? {} : { summary }), - status, - ...(topicAttestations.items.length === 0 - ? {} - : { topics: topicAttestations.items.map((item) => item.id) }), - ...(relationType === "none" - ? {} - : { - relation: buildCreateRelation( - relationType, - relationTargetId, - relationTargetKind, - relationTargetTitle, - )!, - }), - } - : { - kind: "spec.update", - target: { - id: targetId, - kind: targetKind, - ...(targetTitle === "" ? {} : { title: targetTitle }), - }, - patch: { - ...(includeTitle ? { title } : {}), - ...(includeSummary ? { summary } : {}), - ...(includeBody ? { body } : {}), - }, - }, - rationale, - evidence: [ - ...preservedEvidence, - ...(evidenceNote === "" ? [] : [{ kind: "manual" as const, note: evidenceNote }]), - ], - targetRevisions: mode === "create" ? createTargetRevisions : [{ - target: { kind: "entity", id: targetId }, - revision: targetRevision, - semanticRevision: semanticRevisionNumber, - }], - }); + const draftInput = (): InboxDraftInput => { + const createFields = { + title, body, ...(summary === "" ? {} : { summary }), status, + ...(topicAttestations.items.length === 0 ? {} : { topics: topicAttestations.items.map((item) => item.id) }), + }; + const patch = { + ...(includeTitle ? { title } : {}), + ...(includeSummary ? { summary } : {}), + ...(includeBody ? { body } : {}), + }; + const target = { id: targetId, ...(targetTitle === "" ? {} : { title: targetTitle }) }; + return { + change: mode === "create" + ? legacy + ? { kind: "spec.create", entityKind: entityKind as InboxSpecKind, ...createFields, + ...(relationType === "none" ? {} : { + relation: buildCreateRelation(relationType, relationTargetId, relationTargetKind, relationTargetTitle)!, + }) } + : { kind: "knowledge.create", entityKind: entityKind as InboxKnowledgeKind, ...createFields } + : legacy + ? { kind: "spec.update", target: { ...target, kind: targetKind as InboxSpecKind }, patch } + : { kind: "knowledge.update", target: { ...target, kind: targetKind as InboxKnowledgeKind }, patch }, + rationale, + evidence: [...preservedEvidence, ...(evidenceNote === "" ? [] : [{ kind: "manual" as const, note: evidenceNote }])], + targetRevisions: mode === "create" ? createTargetRevisions : [{ + target: { kind: "entity", id: targetId }, revision: targetRevision, semanticRevision: semanticRevisionNumber, + }], + }; + }; const request = (): InboxOperationPreviewRequest => repair === null ? { @@ -666,7 +729,7 @@ function DraftEditorDialog({ const editorTitle = isRepair ? "Repair proposal manually" - : draft === null ? "Create local Spec draft" : "Edit local Spec draft"; + : draft === null ? "Create local knowledge draft" : `Edit local ${subject} draft`; const previewLabel = isRepair ? "Review repaired proposal" : "Preview local draft"; const wordingInitialFocusRef = mode === "create" || includeTitle ? titleRef @@ -690,8 +753,8 @@ function DraftEditorDialog({ {editorTitle} {isRepair - ? "Update the proposal marked Needs refresh against current Spec content. Repair returns it to teammate review without writing the Spec." - : "This draft stays private to this checkout. Saving it does not publish a proposal or change the Spec."} + ? `Update this proposal against current ${subject} content. Repair returns it to teammate review without changing project knowledge.` + : "This draft stays private to this checkout. Saving does not publish a proposal or change project knowledge."}
    @@ -703,38 +766,53 @@ function DraftEditorDialog({ className={styles.select} disabled={draft !== null || isRepair} id="draft-change-kind" - onChange={(event) => change(setMode, event.currentTarget.value as "create" | "update")} + onChange={(event) => { + targetGeneration.current += 1; + targetLookup.reset(); + change(setMode, event.currentTarget.value as "create" | "update"); + }} ref={changeTypeRef} value={mode} > - Create a Spec entity - Update a Spec entity + Create knowledge + Update knowledge - Spec kind + {legacy ? "Spec kind" : "Knowledge kind"} change( - mode === "create" ? setEntityKind : setTargetKind, - event.currentTarget.value as InboxSpecKind, - )} + onChange={(event) => { + change(mode === "create" ? setEntityKind : setTargetKind, event.currentTarget.value as InboxEntityKind); + if (mode === "update" && !legacy) { + targetGeneration.current += 1; + targetLookup.reset(); + setTargetId(""); setTargetTitle(""); setTargetRevision(""); setTargetNotice(""); + } + }} ref={entityKindRef} value={mode === "create" ? entityKind : targetKind} > - {specKinds.map((kind) => ( + {(legacy ? specKinds : inboxKnowledgeKinds).map((kind) => ( {sentenceCase(kind)} ))}
    + {mode === "update" && !legacy && isInboxKnowledgeKind(targetKind) ? <> + + {targetLookup.isError ? The current record could not be read. Choose it again to retry. : null} + {targetNotice ? {targetNotice} : null} + : null} {mode === "update" ? ( Included patch fields -
    +