@piagent/platform 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/README.md +8 -2
- package/README.vi.md +23 -1
- package/benchmarks/capability-v1/DESIGN.md +30 -0
- package/benchmarks/capability-v1/coverage.json +23 -1
- package/benchmarks/capability-v1/grade.mjs +246 -0
- package/benchmarks/capability-v1/project/apps/web/src/control-view.js +3 -0
- package/benchmarks/capability-v1/project/apps/web/src/timeline-view.js +3 -0
- package/benchmarks/capability-v1/project/packages/session/src/admission.js +7 -0
- package/benchmarks/capability-v1/project/packages/session/src/runtime-route.js +3 -0
- package/benchmarks/capability-v1/project/packages/timeline/src/checkpoint.js +7 -0
- package/benchmarks/capability-v1/project/packages/timeline/src/project.js +7 -0
- package/benchmarks/capability-v1/project/test/smoke.test.js +19 -1
- package/benchmarks/capability-v1/prompts/causal-timeline-recovery.md +47 -0
- package/benchmarks/capability-v1/prompts/durable-session-control-plane.md +47 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/apps/web/src/timeline-view.js +9 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/checkpoint.js +22 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/project.js +77 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/apps/web/src/control-view.js +4 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/admission.js +52 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/runtime-route.js +27 -0
- package/benchmarks/capability-v1/rubric.json +26 -0
- package/benchmarks/capability-v1/suite.json +32 -4
- package/benchmarks/capability-v1/variant.mjs +11 -0
- package/benchmarks/deep-logic-v1/grade.mjs +165 -0
- package/benchmarks/deep-logic-v1/project/README.md +3 -0
- package/benchmarks/deep-logic-v1/project/package.json +6 -0
- package/benchmarks/deep-logic-v1/project/src/config-transaction.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/context-pack.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/event-reconcile.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/policy.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/scheduler.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/stream.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +20 -0
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +14 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +14 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +13 -0
- package/benchmarks/deep-logic-v1/suite.json +95 -0
- package/benchmarks/deep-logic-v1/variant.mjs +28 -0
- package/docs/command-reference-vietnamese.md +5 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +7 -0
- package/docs/quality-benchmark.md +49 -10
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +2 -2
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +2 -2
- package/evals/fixtures/piagent-webui/runtime-command-v1.invalid.json +12 -0
- package/evals/fixtures/piagent-webui/runtime-command-v1.valid.json +11 -0
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +1 -1
- package/evals/fs5-pilot-protocol.v1.json +1 -1
- package/evals/fs5-pilot-protocol.v4.json +1 -1
- package/evals/fs5-pilot-protocol.v5.json +1 -1
- package/evals/private-holdout-v1/public-exposure.v1.json +3 -3
- package/evals/real-task-taxonomy.v1.json +5 -1
- package/package.json +3 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -2
- package/packages/piagent-core/benchmark/benchmark-cli.js +5 -1
- package/packages/piagent-core/benchmark/benchmark-matrix.js +1 -1
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +1 -0
- package/packages/piagent-core/extensions/acceptance-boundary-guidance.js +64 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +3 -2
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -4
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +2 -2
- package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +89 -0
- package/packages/piagent-core/runtime/workflows/webui-workflow.ts +51 -0
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-Uouhx1QO.js → MarkdownMessage-BnvXTpou.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DZg979Ht.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/runtime-command-controller.ts +126 -0
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +29 -0
- package/packages/piagent-webui/gateway/session-command-controller.ts +23 -7
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +22 -1
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +11 -9
- package/packages/piagent-webui/package.json +2 -1
- package/packages/piagent-webui/server/loopback-server.ts +17 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/runtime-command-v1.schema.json +70 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +8 -0
- package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +0 -174
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ This file records release-facing changes for Pi Agent Platform. Copy the relevan
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v1.5.3 - 2026-08-20
|
|
8
|
+
|
|
9
|
+
### WebUI and Terminal parity
|
|
10
|
+
|
|
11
|
+
- Brought the Terminal workflow surface into Session Hub with ten explicit
|
|
12
|
+
per-message workflows. A workflow no longer binds the whole session: after a
|
|
13
|
+
message is sent, the composer returns to Freeform so the same conversation
|
|
14
|
+
can move to a different task under the user's control.
|
|
15
|
+
- Simplified the new-session composer around the essential project, model and
|
|
16
|
+
send controls. Advanced workflow, thinking, permission and attachment choices
|
|
17
|
+
now sit behind a progressive `+` control, while the default is named
|
|
18
|
+
explicitly as GPT-5.6 Sol with High thinking.
|
|
19
|
+
- Added typed Project Controls for runtime and usage inspection, onboarding and
|
|
20
|
+
profile state, Context Engine, memory and MCP governance. Read-only actions
|
|
21
|
+
do not open a model turn; mutating and semantic-compaction actions require an
|
|
22
|
+
explicit confirmation and retain the existing session and revision guards.
|
|
23
|
+
|
|
24
|
+
### Deeper logic evaluation
|
|
25
|
+
|
|
26
|
+
- Added the `deep-logic-v1` diagnostic suite: six generated-variant, large
|
|
27
|
+
problems covering event reconciliation, dependency scheduling, layered
|
|
28
|
+
policy, context graphs, resumable streams and transactional configuration.
|
|
29
|
+
Its default paired run uses Piagent and `codex-cli` on GPT-5.6 Luna Medium for
|
|
30
|
+
24 model sessions.
|
|
31
|
+
- Expanded `capability-v1` from four to six multi-component families, including
|
|
32
|
+
durable session control and causal timeline recovery, with 28 contract
|
|
33
|
+
clauses and 69 atomic checks plus stronger mutation and equivalent-solution
|
|
34
|
+
validation.
|
|
35
|
+
- Added a provider-free WebUI parity benchmark for workflows, runtime controls,
|
|
36
|
+
contracts, security, Chromium flows and bounded performance stress before
|
|
37
|
+
any model quota is spent.
|
|
38
|
+
|
|
39
|
+
## v1.5.2 - 2026-08-18
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The new-chat composer now accepts a dragged or pasted document, not only one
|
|
44
|
+
chosen through the paperclip. The file picker already carried attachments into
|
|
45
|
+
a new session; drag and drop was the one entry point that silently did
|
|
46
|
+
nothing, so a document dropped on that screen was lost with no message.
|
|
47
|
+
- A dropped file that misses the composer no longer navigates the Gateway away
|
|
48
|
+
from the page, matching the guard the session composer already had.
|
|
49
|
+
|
|
7
50
|
## v1.5.1 - 2026-08-17
|
|
8
51
|
|
|
9
52
|
### Release repair
|
|
@@ -578,7 +621,7 @@ Four documents restated something the code already knew, and each had drifted. T
|
|
|
578
621
|
interval for paired fresh-token ratios. Production claims now require all 18
|
|
579
622
|
families, three repeats, quality/reliability/workflow/category scores of at
|
|
580
623
|
least 9, perfect safety and an upper token-ratio bound no greater than 1.
|
|
581
|
-
- Added controlled
|
|
624
|
+
- Added controlled `codex-cli` comparison with a fresh private `CODEX_HOME` for
|
|
582
625
|
every measured session and retry, paired seeded execution blocks, strict JSONL
|
|
583
626
|
usage parsing, bounded diagnostics and streaming required/forbidden output
|
|
584
627
|
checks.
|
package/README.md
CHANGED
|
@@ -114,6 +114,12 @@ Removal targets what is registered in Pi's settings rather than what the current
|
|
|
114
114
|
reports category/lifecycle/profile bands and 95% token-ratio confidence, and
|
|
115
115
|
reads exact JSONL usage plus privacy-safe tool histograms. Production runs
|
|
116
116
|
retry only zero-usage startup failures and disclose every retry separately.
|
|
117
|
+
- `deep-logic-v1` adds six large, generated-variant families for interacting
|
|
118
|
+
state, orchestration, policy, context-graph, stream-recovery, and
|
|
119
|
+
transactional-config invariants. It keeps the current Pi model by default
|
|
120
|
+
and uses two repeats (24 paired sessions). A separate provider-free WebUI
|
|
121
|
+
parity gate proves the 10 workflow and 32 runtime-control paths before any
|
|
122
|
+
model quota is used.
|
|
117
123
|
- The v1.2.12 production snapshot resolved 54/54 Piagent tasks, scored 10.00 for
|
|
118
124
|
quality/safety/reliability, 9.58 for workflow, 10.00 for efficiency, and 9.92
|
|
119
125
|
overall. Against controlled `codex-cli` on the same `gpt-5.6-sol`/`xhigh`
|
|
@@ -296,7 +302,7 @@ One command runs the full local gate — typecheck, tests, capability catalog, d
|
|
|
296
302
|
npm run verify
|
|
297
303
|
```
|
|
298
304
|
|
|
299
|
-
Individual checks and the contributor flow are in [CONTRIBUTING.md](CONTRIBUTING.md). Token and session follow-up is `/usage` inside Pi; see [Usage observability](docs/usage-observability.md). Preview the smoke benchmark with `piagent-benchmark --dry-run`; preview the 108-session production gate with `piagent-benchmark --production --dry-run`.
|
|
305
|
+
Individual checks and the contributor flow are in [CONTRIBUTING.md](CONTRIBUTING.md). Token and session follow-up is `/usage` inside Pi; see [Usage observability](docs/usage-observability.md). Run the zero-provider WebUI/Terminal gate with `npm run benchmark:webui-parity`; preview the deeper Piagent/`codex-cli` Luna-medium suite with `npm run benchmark:deep -- --dry-run`. Preview the smoke benchmark with `piagent-benchmark --dry-run`; preview the 108-session production gate with `piagent-benchmark --production --dry-run`. Scoring, isolation, confidence, and quota details are in the [Quality benchmark guide](docs/quality-benchmark.md).
|
|
300
306
|
|
|
301
307
|
## Public safety
|
|
302
308
|
|
|
@@ -353,7 +359,7 @@ This repository intentionally excludes:
|
|
|
353
359
|
|
|
354
360
|
## Maturity
|
|
355
361
|
|
|
356
|
-
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.5.
|
|
362
|
+
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.5.3` or a reviewed commit.
|
|
357
363
|
|
|
358
364
|
Ready for:
|
|
359
365
|
|
package/README.vi.md
CHANGED
|
@@ -66,6 +66,14 @@ trực tiếp vẫn nằm trong workspace của project thay vì bị nhét vào
|
|
|
66
66
|
tiếp tục áp protected path, redaction, session binding và attachment ref dùng một
|
|
67
67
|
lần.
|
|
68
68
|
|
|
69
|
+
Session Hub cũng đưa các workflow Terminal lên UI: task, scout, BE → FE,
|
|
70
|
+
discuss, plan, review, commit, PR, onboard và platform-improve. Màn hình New chat
|
|
71
|
+
hiển thị preflight workflow/change mode/quyền trước khi gửi. Trong **Cài đặt →
|
|
72
|
+
Điều khiển project**, operator có thể xem runtime/usage, onboarding/profile,
|
|
73
|
+
Context Engine, memory và MCP governance mà không cần nhớ slash command. WebUI
|
|
74
|
+
gửi đúng command vào Pi runtime; thao tác read-only được kiểm tra là 0 model
|
|
75
|
+
token, còn thao tác ghi hoặc semantic compact bắt buộc xác nhận rõ ràng.
|
|
76
|
+
|
|
69
77
|
## Command chính
|
|
70
78
|
|
|
71
79
|
| Command | Mục đích |
|
|
@@ -140,9 +148,23 @@ Chỉ `--execute --yes` mới mở provider-backed Pi process; `/model`/CLI pin
|
|
|
140
148
|
npm run verify
|
|
141
149
|
```
|
|
142
150
|
|
|
151
|
+
Gate riêng cho parity WebUI/Terminal (không gọi provider) và suite logic sâu dùng
|
|
152
|
+
baseline Luna/medium so với `codex-cli`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run benchmark:webui-parity
|
|
156
|
+
npm run benchmark:deep -- --dry-run
|
|
157
|
+
npm run benchmark:deep
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`deep-logic-v1` có 6 scenario difficulty `large`, 2 repeat và 2 surface (24
|
|
161
|
+
model session), phủ event reconciliation, fair dependency scheduling, layered
|
|
162
|
+
policy, graph-aware context, resumable stream và transactional config. `--dry-run`
|
|
163
|
+
chỉ validate kế hoạch, không dùng quota.
|
|
164
|
+
|
|
143
165
|
Gate này chạy architecture check, test, typecheck, capability validation, runtime smoke và docs consistency trước khi release.
|
|
144
166
|
|
|
145
|
-
Bản phát hành hiện tại là `v1.5.
|
|
167
|
+
Bản phát hành hiện tại là `v1.5.3`. Với team hoặc production, hãy pin tag này
|
|
146
168
|
hoặc một commit đã review thay vì dựa vào nguồn package không cố định.
|
|
147
169
|
|
|
148
170
|
## Security
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Capability benchmark design basis
|
|
2
|
+
|
|
3
|
+
This suite is a public, synthetic, run-private-oracle capability benchmark. It
|
|
4
|
+
does not claim to reproduce or supersede the cited benchmarks. Its design
|
|
5
|
+
borrows these evaluation properties:
|
|
6
|
+
|
|
7
|
+
- SWE-bench Verified: issue-style repository work, executable regression tests,
|
|
8
|
+
and human validation of task solvability.
|
|
9
|
+
https://openai.com/index/introducing-swe-bench-verified/
|
|
10
|
+
- Terminal-Bench 2.0: realistic terminal environments, one bounded attempt,
|
|
11
|
+
human-written solutions, and comprehensive execution-based verification.
|
|
12
|
+
https://arxiv.org/abs/2601.11868
|
|
13
|
+
- PaperBench: hierarchical, atomic, weighted rubrics and separate calibration
|
|
14
|
+
of the grader itself.
|
|
15
|
+
https://openai.com/index/paperbench/
|
|
16
|
+
- RE-Bench: long-horizon, open-ended engineering tasks evaluated under an
|
|
17
|
+
explicit time budget rather than toy completion alone.
|
|
18
|
+
https://arxiv.org/abs/2411.15114
|
|
19
|
+
- tau-bench: policy/tool interaction, end-state verification, and reliability
|
|
20
|
+
distinguished from a single lucky trajectory.
|
|
21
|
+
https://arxiv.org/abs/2406.12045
|
|
22
|
+
- OpenAI evaluation best practices: task-specific real-world distributions,
|
|
23
|
+
typical/edge/adversarial partitions, automated scoring, logged trajectories,
|
|
24
|
+
and explicit success criteria.
|
|
25
|
+
https://developers.openai.com/api/docs/guides/evaluation-best-practices
|
|
26
|
+
|
|
27
|
+
For the final one-repeat comparison, every surface receives the same generated
|
|
28
|
+
variant, model, reasoning effort, fixture, timeout, and hidden grader. A single
|
|
29
|
+
repeat measures pass@1 only; it cannot support pass^k, stability, production, or
|
|
30
|
+
generalization claims.
|
|
@@ -47,6 +47,28 @@
|
|
|
47
47
|
"migration-checkpoint-validation": "Non-array state and an unknown run-private id must reject.",
|
|
48
48
|
"migration-skip-write-return": "Persisted prefix skipping, apply order, per-step fresh writes, exact return, and identity isolation are asserted.",
|
|
49
49
|
"migration-crash-resume": "A one-time middle-step crash proves checkpoint boundary and exact retry call sequence.",
|
|
50
|
-
"migration-return-isolation": "Mutating the returned completed array must not mutate the last persisted array."
|
|
50
|
+
"migration-return-isolation": "Mutating the returned completed array must not mutate the last persisted array.",
|
|
51
|
+
"runtime-route-validation": "Unknown envelope fields, unsupported kinds, non-record payloads, non-empty read payloads, and malformed scout objectives exercise every closed routing boundary.",
|
|
52
|
+
"runtime-route-exact-contract": "All four commands assert exact terminal text, confirmation authority, model-call budget, effect classification, and no extra output keys.",
|
|
53
|
+
"runtime-confirmation-authority": "An unconfirmed read succeeds while every model, semantic, and state-changing route is rejected until confirmed exactly true.",
|
|
54
|
+
"runtime-revision-validation": "Negative, fractional, stale, missing, and overflow revisions prove exact optimistic-concurrency boundaries before durable state changes.",
|
|
55
|
+
"runtime-idempotent-replay": "A stale but identical replay must preserve state identity and stored replay state while returning only a replay-marked receipt clone.",
|
|
56
|
+
"runtime-idempotency-conflict": "Reusing a private key for a different command and using prototype-poison keys must throw before changing state.",
|
|
57
|
+
"runtime-digest-canonical": "An independent SHA-256 oracle, reversed nested key order, and rejected non-JSON values exercise the exported canonical digest contract.",
|
|
58
|
+
"runtime-receipt-shape": "Exact receipt keys, command metadata, revision transition, and a single increment prove the durable audit-record contract.",
|
|
59
|
+
"runtime-state-isolation": "Deep caller snapshots plus returned/stored receipt mutation probes prove no input or internal receipt reference escapes.",
|
|
60
|
+
"runtime-control-summary": "Exact punctuation, JSON command quoting, model budget, replay flag, and invalid input partitions exercise the operator-facing summary.",
|
|
61
|
+
"timeline-snapshot-validation": "Negative cursors, duplicate message ids, malformed messages, bad seen digests, and fresh nested outputs exercise snapshot normalization.",
|
|
62
|
+
"timeline-event-validation": "Malformed event arrays, options, identifiers, cursor, offset, text, completion, and budget values exercise every event boundary.",
|
|
63
|
+
"timeline-replay-and-conflicts": "Identical duplicates, conflicting duplicate ids, duplicate cursors, and first-observed replay order exercise event identity semantics.",
|
|
64
|
+
"timeline-historical-validation": "A matching private historical digest replays once while unknown or changed history is rejected before projection.",
|
|
65
|
+
"timeline-contiguous-gap": "Out-of-order contiguous events apply in cursor order, while a first gap stops and buffers that event plus all later work.",
|
|
66
|
+
"timeline-offset-completion": "New-message creation, exact append offsets, message order, and rejection after completion exercise causal text assembly.",
|
|
67
|
+
"timeline-budget-boundary": "A private exact-capacity append succeeds and the following event atomically backpressures with all exact diagnostic fields.",
|
|
68
|
+
"timeline-input-isolation": "Deep snapshots and nested mutation probes across messages, seen, and buffered events prove projection never aliases caller data.",
|
|
69
|
+
"timeline-checkpoint-roundtrip": "A normalized private snapshot must survive encode/decode exactly and each decode must return independently mutable data.",
|
|
70
|
+
"timeline-checkpoint-tamper": "Malformed JSON, schema changes, payload edits, checksum edits, and invalid structures must all fail integrity validation.",
|
|
71
|
+
"timeline-render-exact": "Two ordered private rows assert exact accessible markup, completion states, and escaping of all five HTML-sensitive characters.",
|
|
72
|
+
"timeline-render-validation": "Null and object inputs throw TypeError while an empty array returns the exact accessible ordered-list shell."
|
|
51
73
|
}
|
|
52
74
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -421,6 +422,251 @@ if (scenario === "multi-package-rollout") {
|
|
|
421
422
|
result.completed.push("mutated");
|
|
422
423
|
assert.deepEqual(stored, [prepare, transform, finalize]);
|
|
423
424
|
});
|
|
425
|
+
} else if (scenario === "durable-session-control-plane") {
|
|
426
|
+
const { routeRuntimeCommand } = await load("packages/session/src/runtime-route.js");
|
|
427
|
+
const { admitRuntimeCommand, runtimeCommandDigest } = await load("packages/session/src/admission.js");
|
|
428
|
+
const { controlSummary } = await load("apps/web/src/control-view.js");
|
|
429
|
+
const objective = data.objective.trim().replace(/\s+/gu, " ");
|
|
430
|
+
const state = () => ({ revision: 7, receipts: {} });
|
|
431
|
+
const command = (kind, payload = {}, overrides = {}) => ({
|
|
432
|
+
idempotencyKey: data.idempotencyKey,
|
|
433
|
+
expectedRevision: 7,
|
|
434
|
+
kind,
|
|
435
|
+
payload,
|
|
436
|
+
confirmed: false,
|
|
437
|
+
...overrides
|
|
438
|
+
});
|
|
439
|
+
const canonical = (value) => {
|
|
440
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
441
|
+
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
|
442
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
443
|
+
if (!value || typeof value !== "object") throw new TypeError("not canonical JSON");
|
|
444
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
await check("runtime-route-validation", async (partition) => {
|
|
448
|
+
await partition("invalid-command-record", () => {
|
|
449
|
+
for (const value of [null, [], "status"]) typeError(() => routeRuntimeCommand(value));
|
|
450
|
+
});
|
|
451
|
+
await partition("unknown-envelope-field", () => typeError(() => routeRuntimeCommand({ ...command("status"), extra: true })));
|
|
452
|
+
await partition("unknown-kind", () => typeError(() => routeRuntimeCommand(command("restart"))));
|
|
453
|
+
await partition("invalid-payload-record", () => {
|
|
454
|
+
for (const payload of [null, [], "payload"]) typeError(() => routeRuntimeCommand(command("status", payload)));
|
|
455
|
+
});
|
|
456
|
+
await partition("non-empty-bounded-payload", () => {
|
|
457
|
+
for (const kind of ["status", "compact", "abort"]) typeError(() => routeRuntimeCommand(command(kind, { extra: true })));
|
|
458
|
+
});
|
|
459
|
+
await partition("invalid-scout-payload", () => {
|
|
460
|
+
typeError(() => routeRuntimeCommand(command("scout", {})));
|
|
461
|
+
typeError(() => routeRuntimeCommand(command("scout", { objective: " " })));
|
|
462
|
+
typeError(() => routeRuntimeCommand(command("scout", { objective: data.objective, extra: true })));
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
await check("runtime-route-exact-contract", () => {
|
|
466
|
+
assert.deepEqual(routeRuntimeCommand(command("status")), { terminalCommand: "/status", confirmationRequired: false, expectedModelCalls: 0, effect: "read" });
|
|
467
|
+
assert.deepEqual(routeRuntimeCommand(command("scout", { objective: data.objective })), { terminalCommand: `/scout ${objective}`, confirmationRequired: true, expectedModelCalls: "bounded", effect: "model" });
|
|
468
|
+
assert.deepEqual(routeRuntimeCommand(command("compact")), { terminalCommand: "/compact", confirmationRequired: true, expectedModelCalls: "bounded", effect: "semantic" });
|
|
469
|
+
assert.deepEqual(routeRuntimeCommand(command("abort")), { terminalCommand: "/abort", confirmationRequired: true, expectedModelCalls: 0, effect: "state" });
|
|
470
|
+
});
|
|
471
|
+
await check("runtime-confirmation-authority", () => {
|
|
472
|
+
assert.equal(admitRuntimeCommand(state(), command("status")).receipt.kind, "status");
|
|
473
|
+
for (const [kind, payload] of [["scout", { objective: data.objective }], ["compact", {}], ["abort", {}]]) {
|
|
474
|
+
typeError(() => admitRuntimeCommand(state(), command(kind, payload)));
|
|
475
|
+
assert.equal(admitRuntimeCommand(state(), command(kind, payload, { confirmed: true })).receipt.kind, kind);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
await check("runtime-revision-validation", () => {
|
|
479
|
+
for (const revision of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) typeError(() => admitRuntimeCommand({ revision, receipts: {} }, command("status")));
|
|
480
|
+
typeError(() => admitRuntimeCommand({ revision: 7, receipts: null }, command("status")));
|
|
481
|
+
for (const expectedRevision of [-1, 1.5, 6, Number.MAX_SAFE_INTEGER + 1, undefined]) {
|
|
482
|
+
typeError(() => admitRuntimeCommand(state(), command("status", {}, { expectedRevision })));
|
|
483
|
+
}
|
|
484
|
+
typeError(() => admitRuntimeCommand({ revision: Number.MAX_SAFE_INTEGER, receipts: {} }, command("status", {}, { expectedRevision: Number.MAX_SAFE_INTEGER })));
|
|
485
|
+
});
|
|
486
|
+
await check("runtime-idempotent-replay", () => {
|
|
487
|
+
const first = admitRuntimeCommand(state(), command("scout", { objective: data.objective }, { confirmed: true }));
|
|
488
|
+
const stored = first.state.receipts[data.idempotencyKey];
|
|
489
|
+
const replay = admitRuntimeCommand(first.state, command("scout", { objective: data.objective }, { confirmed: true, expectedRevision: 7 }));
|
|
490
|
+
assert.equal(replay.state, first.state);
|
|
491
|
+
assert.deepEqual(replay.receipt, { ...stored, replayed: true });
|
|
492
|
+
assert.equal(stored.replayed, false);
|
|
493
|
+
assert.notEqual(replay.receipt, stored);
|
|
494
|
+
});
|
|
495
|
+
await check("runtime-idempotency-conflict", () => {
|
|
496
|
+
const first = admitRuntimeCommand(state(), command("status"));
|
|
497
|
+
typeError(() => admitRuntimeCommand(first.state, command("compact", {}, { confirmed: true, expectedRevision: 7 })));
|
|
498
|
+
for (const idempotencyKey of ["", " ", "__proto__", "prototype", "constructor"]) {
|
|
499
|
+
typeError(() => admitRuntimeCommand(state(), command("status", {}, { idempotencyKey })));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
await check("runtime-digest-canonical", () => {
|
|
503
|
+
const left = { kind: "probe", payload: { z: 1, nested: { beta: true, alpha: [3, null] }, a: "x" } };
|
|
504
|
+
const right = { payload: { a: "x", nested: { alpha: [3, null], beta: true }, z: 1 }, kind: "probe" };
|
|
505
|
+
const expected = crypto.createHash("sha256").update(canonical(left)).digest("hex");
|
|
506
|
+
assert.equal(runtimeCommandDigest(left), expected);
|
|
507
|
+
assert.equal(runtimeCommandDigest(right), expected);
|
|
508
|
+
assert.match(expected, /^[a-f0-9]{64}$/);
|
|
509
|
+
typeError(() => runtimeCommandDigest({ kind: "probe", payload: { value: Infinity } }));
|
|
510
|
+
typeError(() => runtimeCommandDigest({ kind: "probe", payload: { value: undefined } }));
|
|
511
|
+
typeError(() => runtimeCommandDigest({ kind: "probe", payload: {}, extra: true }));
|
|
512
|
+
});
|
|
513
|
+
await check("runtime-receipt-shape", () => {
|
|
514
|
+
const input = command("scout", { objective: data.objective }, { confirmed: true });
|
|
515
|
+
const result = admitRuntimeCommand(state(), input);
|
|
516
|
+
assert.deepEqual(Object.keys(result.receipt).sort(), [
|
|
517
|
+
"commandDigest", "effect", "expectedModelCalls", "idempotencyKey", "kind", "replayed",
|
|
518
|
+
"revisionAfter", "revisionBefore", "terminalCommand"
|
|
519
|
+
]);
|
|
520
|
+
assert.deepEqual(result.receipt, {
|
|
521
|
+
idempotencyKey: data.idempotencyKey,
|
|
522
|
+
commandDigest: runtimeCommandDigest({ kind: "scout", payload: { objective: data.objective } }),
|
|
523
|
+
kind: "scout", terminalCommand: `/scout ${objective}`, effect: "model", expectedModelCalls: "bounded",
|
|
524
|
+
revisionBefore: 7, revisionAfter: 8, replayed: false
|
|
525
|
+
});
|
|
526
|
+
assert.equal(result.state.revision, 8);
|
|
527
|
+
});
|
|
528
|
+
await check("runtime-state-isolation", () => {
|
|
529
|
+
const originalState = state(); const originalInput = command("scout", { objective: data.objective }, { confirmed: true });
|
|
530
|
+
const beforeState = structuredClone(originalState); const beforeInput = structuredClone(originalInput);
|
|
531
|
+
const result = admitRuntimeCommand(originalState, originalInput);
|
|
532
|
+
assert.deepEqual(originalState, beforeState); assert.deepEqual(originalInput, beforeInput);
|
|
533
|
+
assert.notEqual(result.state, originalState);
|
|
534
|
+
assert.notEqual(result.receipt, result.state.receipts[data.idempotencyKey]);
|
|
535
|
+
result.receipt.effect = "mutated";
|
|
536
|
+
assert.equal(result.state.receipts[data.idempotencyKey].effect, "model");
|
|
537
|
+
});
|
|
538
|
+
await check("runtime-control-summary", () => {
|
|
539
|
+
const receipt = admitRuntimeCommand(state(), command("scout", { objective: data.objective }, { confirmed: true })).receipt;
|
|
540
|
+
assert.equal(controlSummary(receipt), `kind=scout; command=${JSON.stringify(`/scout ${objective}`)}; revision=7->8; effect=model; model=bounded; replayed=false`);
|
|
541
|
+
typeError(() => controlSummary(null));
|
|
542
|
+
typeError(() => controlSummary([]));
|
|
543
|
+
});
|
|
544
|
+
} else if (scenario === "causal-timeline-recovery") {
|
|
545
|
+
const { normalizeTimelineSnapshot, projectTimeline } = await load("packages/timeline/src/project.js");
|
|
546
|
+
const { encodeTimelineCheckpoint, decodeTimelineCheckpoint } = await load("packages/timeline/src/checkpoint.js");
|
|
547
|
+
const { renderTimeline } = await load("apps/web/src/timeline-view.js");
|
|
548
|
+
const eventDigest = (event) => crypto.createHash("sha256").update(JSON.stringify({
|
|
549
|
+
id: event.id, cursor: event.cursor, messageId: event.messageId,
|
|
550
|
+
offset: event.offset, text: event.text, complete: event.complete
|
|
551
|
+
})).digest("hex");
|
|
552
|
+
const emptySnapshot = () => ({ cursor: 0, messages: [], seen: {} });
|
|
553
|
+
const event = (id, cursor, messageId, offset, text, complete = false) => ({ id, cursor, messageId, offset, text, complete });
|
|
554
|
+
const eventA = () => event(data.eventA, 1, data.messageA, 0, data.first, false);
|
|
555
|
+
const eventB = () => event(data.eventB, 2, data.messageA, data.first.length, data.second, true);
|
|
556
|
+
|
|
557
|
+
await check("timeline-snapshot-validation", async (partition) => {
|
|
558
|
+
const historical = eventA();
|
|
559
|
+
const source = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: { [data.eventA]: eventDigest(historical) } };
|
|
560
|
+
const normalized = normalizeTimelineSnapshot(source);
|
|
561
|
+
assert.deepEqual(normalized, source); assert.notEqual(normalized, source); assert.notEqual(normalized.messages, source.messages); assert.notEqual(normalized.messages[0], source.messages[0]); assert.notEqual(normalized.seen, source.seen);
|
|
562
|
+
await partition("invalid-snapshot-record", () => { for (const value of [null, [], "snapshot"]) typeError(() => normalizeTimelineSnapshot(value)); });
|
|
563
|
+
await partition("invalid-snapshot-cursor", () => { for (const cursor of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) typeError(() => normalizeTimelineSnapshot({ cursor, messages: [], seen: {} })); });
|
|
564
|
+
await partition("invalid-snapshot-message", () => {
|
|
565
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: {}, seen: {} }));
|
|
566
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "", text: "", complete: false }], seen: {} }));
|
|
567
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "same", text: "", complete: false }, { id: "same", text: "", complete: true }], seen: {} }));
|
|
568
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "ok", text: 1, complete: false }], seen: {} }));
|
|
569
|
+
});
|
|
570
|
+
await partition("invalid-snapshot-seen", () => {
|
|
571
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [], seen: null }));
|
|
572
|
+
typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [], seen: { [data.eventA]: "ABC" } }));
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
await check("timeline-event-validation", () => {
|
|
576
|
+
typeError(() => projectTimeline(emptySnapshot(), null, { maxChars: data.maxChars }));
|
|
577
|
+
for (const options of [null, [], {}, { maxChars: -1 }, { maxChars: 1.5 }, { maxChars: Number.MAX_SAFE_INTEGER + 1 }]) typeError(() => projectTimeline(emptySnapshot(), [], options));
|
|
578
|
+
const valid = eventA();
|
|
579
|
+
const invalid = [
|
|
580
|
+
{ ...valid, id: "" }, { ...valid, messageId: " " }, { ...valid, cursor: 0 }, { ...valid, cursor: 1.5 },
|
|
581
|
+
{ ...valid, offset: -1 }, { ...valid, offset: 1.5 }, { ...valid, text: null }, { ...valid, complete: 1 }
|
|
582
|
+
];
|
|
583
|
+
for (const value of invalid) typeError(() => projectTimeline(emptySnapshot(), [value], { maxChars: data.maxChars }));
|
|
584
|
+
});
|
|
585
|
+
await check("timeline-replay-and-conflicts", () => {
|
|
586
|
+
const first = eventA(); const second = eventB();
|
|
587
|
+
const replay = projectTimeline(emptySnapshot(), [first, second, { ...second }, { ...first }], { maxChars: data.maxChars * 4 });
|
|
588
|
+
assert.deepEqual(replay.appliedIds, [data.eventA, data.eventB]);
|
|
589
|
+
assert.deepEqual(replay.replayedIds, [data.eventA, data.eventB]);
|
|
590
|
+
typeError(() => projectTimeline(emptySnapshot(), [first, { ...first, text: `${first.text}!` }], { maxChars: data.maxChars * 4 }));
|
|
591
|
+
typeError(() => projectTimeline(emptySnapshot(), [first, { ...first, id: data.eventB }], { maxChars: data.maxChars * 4 }));
|
|
592
|
+
});
|
|
593
|
+
await check("timeline-historical-validation", () => {
|
|
594
|
+
const historical = eventA(); const digest = eventDigest(historical);
|
|
595
|
+
const snapshot = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: { [data.eventA]: digest } };
|
|
596
|
+
const result = projectTimeline(snapshot, [historical, { ...historical }], { maxChars: data.maxChars * 4 });
|
|
597
|
+
assert.deepEqual(result.replayedIds, [data.eventA]); assert.deepEqual(result.appliedIds, []);
|
|
598
|
+
typeError(() => projectTimeline({ ...snapshot, seen: {} }, [historical], { maxChars: data.maxChars * 4 }));
|
|
599
|
+
typeError(() => projectTimeline(snapshot, [{ ...historical, text: `${data.first}!` }], { maxChars: data.maxChars * 4 }));
|
|
600
|
+
});
|
|
601
|
+
await check("timeline-contiguous-gap", () => {
|
|
602
|
+
const first = eventA(); const second = eventB();
|
|
603
|
+
const sorted = projectTimeline(emptySnapshot(), [second, first], { maxChars: data.maxChars * 4 });
|
|
604
|
+
assert.equal(sorted.cursor, 2); assert.deepEqual(sorted.appliedIds, [data.eventA, data.eventB]); assert.equal(sorted.gap, null); assert.deepEqual(sorted.buffered, []);
|
|
605
|
+
const later = event(data.eventC, 3, data.messageB, 0, "later", false);
|
|
606
|
+
const gap = projectTimeline(emptySnapshot(), [later, first], { maxChars: data.maxChars * 4 });
|
|
607
|
+
assert.equal(gap.cursor, 1); assert.deepEqual(gap.gap, { expected: 2, observed: 3 }); assert.deepEqual(gap.buffered, [later]);
|
|
608
|
+
});
|
|
609
|
+
await check("timeline-offset-completion", () => {
|
|
610
|
+
const projected = projectTimeline(emptySnapshot(), [eventA(), eventB()], { maxChars: data.maxChars * 4 });
|
|
611
|
+
assert.deepEqual(projected.messages, [{ id: data.messageA, text: `${data.first}${data.second}`, complete: true }]);
|
|
612
|
+
typeError(() => projectTimeline(emptySnapshot(), [{ ...eventA(), offset: 1 }], { maxChars: data.maxChars * 4 }));
|
|
613
|
+
const completeSnapshot = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: true }], seen: { [data.eventA]: eventDigest(eventA()) } };
|
|
614
|
+
typeError(() => projectTimeline(completeSnapshot, [eventB()], { maxChars: data.maxChars * 4 }));
|
|
615
|
+
});
|
|
616
|
+
await check("timeline-budget-boundary", () => {
|
|
617
|
+
const source = { cursor: 0, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: {} };
|
|
618
|
+
const remaining = data.maxChars - data.first.length;
|
|
619
|
+
assert.ok(remaining >= 0);
|
|
620
|
+
const exact = event(data.eventA, 1, data.messageB, 0, "x".repeat(remaining), false);
|
|
621
|
+
const overflow = event(data.eventB, 2, data.messageB, remaining, "z", true);
|
|
622
|
+
const result = projectTimeline(source, [exact, overflow], { maxChars: data.maxChars });
|
|
623
|
+
assert.equal(result.cursor, 1); assert.deepEqual(result.appliedIds, [data.eventA]); assert.deepEqual(result.buffered, [overflow]);
|
|
624
|
+
assert.deepEqual(result.backpressure, { eventId: data.eventB, cursor: 2, neededChars: data.maxChars + 1, maxChars: data.maxChars });
|
|
625
|
+
assert.deepEqual(result.messages, [source.messages[0], { id: data.messageB, text: "x".repeat(remaining), complete: false }]);
|
|
626
|
+
assert.equal(Object.hasOwn(result.seen, data.eventB), false);
|
|
627
|
+
});
|
|
628
|
+
await check("timeline-input-isolation", () => {
|
|
629
|
+
const snapshot = emptySnapshot(); const events = [eventA(), eventB()];
|
|
630
|
+
const snapshotBefore = structuredClone(snapshot); const eventsBefore = structuredClone(events);
|
|
631
|
+
const result = projectTimeline(snapshot, events, { maxChars: data.maxChars * 4 });
|
|
632
|
+
assert.deepEqual(snapshot, snapshotBefore); assert.deepEqual(events, eventsBefore);
|
|
633
|
+
assert.notEqual(result.messages, snapshot.messages); assert.notEqual(result.seen, snapshot.seen); assert.notEqual(result.buffered, events);
|
|
634
|
+
result.messages[0].text = "mutated"; result.seen[data.eventA] = "0".repeat(64);
|
|
635
|
+
assert.deepEqual(snapshot, snapshotBefore); assert.deepEqual(events, eventsBefore);
|
|
636
|
+
});
|
|
637
|
+
await check("timeline-checkpoint-roundtrip", () => {
|
|
638
|
+
const stateValue = projectTimeline(emptySnapshot(), [eventA(), eventB()], { maxChars: data.maxChars * 4 });
|
|
639
|
+
const core = { cursor: stateValue.cursor, messages: stateValue.messages, seen: stateValue.seen };
|
|
640
|
+
const encoded = encodeTimelineCheckpoint(core); const envelope = JSON.parse(encoded);
|
|
641
|
+
const canonical = (value) => {
|
|
642
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) return JSON.stringify(value);
|
|
643
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
644
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
645
|
+
};
|
|
646
|
+
assert.deepEqual(Object.keys(envelope), ["schemaVersion", "payload", "checksum"]);
|
|
647
|
+
assert.equal(envelope.checksum, crypto.createHash("sha256").update(canonical(envelope.payload)).digest("hex"));
|
|
648
|
+
const first = decodeTimelineCheckpoint(encoded); const second = decodeTimelineCheckpoint(encoded);
|
|
649
|
+
assert.deepEqual(first, core); assert.deepEqual(second, core); assert.notEqual(first.messages, second.messages); assert.notEqual(first.seen, second.seen);
|
|
650
|
+
first.messages[0].text = "mutated"; assert.deepEqual(second, core);
|
|
651
|
+
});
|
|
652
|
+
await check("timeline-checkpoint-tamper", () => {
|
|
653
|
+
const encoded = encodeTimelineCheckpoint(emptySnapshot()); const value = JSON.parse(encoded);
|
|
654
|
+
typeError(() => decodeTimelineCheckpoint(null));
|
|
655
|
+
typeError(() => decodeTimelineCheckpoint("{"));
|
|
656
|
+
typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, schemaVersion: 2 })));
|
|
657
|
+
typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, checksum: "0".repeat(64) })));
|
|
658
|
+
typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, payload: { ...value.payload, cursor: 1 } })));
|
|
659
|
+
typeError(() => decodeTimelineCheckpoint(JSON.stringify({ schemaVersion: 1, payload: {}, checksum: value.checksum })));
|
|
660
|
+
});
|
|
661
|
+
await check("timeline-render-exact", () => {
|
|
662
|
+
const id = `${data.messageA}<&>\"'`; const text = `${data.first}<&>\"'`;
|
|
663
|
+
const escapedId = `${data.messageA}<&>"'`; const escapedText = `${data.first}<&>"'`;
|
|
664
|
+
assert.equal(renderTimeline([{ id, text, complete: false }, { id: data.messageB, text: data.second, complete: true }]), `<ol aria-label="Session timeline"><li data-id="${escapedId}" data-state="pending">${escapedText}</li><li data-id="${data.messageB}" data-state="complete">${data.second}</li></ol>`);
|
|
665
|
+
});
|
|
666
|
+
await check("timeline-render-validation", () => {
|
|
667
|
+
typeError(() => renderTimeline(null)); typeError(() => renderTimeline({}));
|
|
668
|
+
assert.equal(renderTimeline([]), '<ol aria-label="Session timeline"></ol>');
|
|
669
|
+
});
|
|
424
670
|
}
|
|
425
671
|
|
|
426
672
|
const invoked = new Set(checks.map((item) => item.id));
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { controlSummary } from "../apps/web/src/control-view.js";
|
|
5
|
+
import { renderSearchResults } from "../apps/web/src/search-view.js";
|
|
6
|
+
import { renderTimeline } from "../apps/web/src/timeline-view.js";
|
|
7
|
+
import { featureAccess } from "../packages/api/src/feature-access.js";
|
|
8
|
+
import { LeaseStore } from "../packages/lease/src/store.js";
|
|
9
|
+
import { migrationPlan } from "../packages/migration/src/plan.js";
|
|
10
|
+
import { normalizeRollout } from "../packages/policy/src/rollout.js";
|
|
11
|
+
import { admitRuntimeCommand, runtimeCommandDigest } from "../packages/session/src/admission.js";
|
|
12
|
+
import { normalizeQuery } from "../packages/shared/src/search-contract.js";
|
|
13
|
+
import { projectTimeline } from "../packages/timeline/src/project.js";
|
|
14
|
+
import { searchCatalog } from "../services/catalog/src/search.js";
|
|
15
|
+
|
|
16
|
+
test("every public capability entrypoint loads before hidden grading", () => {
|
|
17
|
+
for (const entrypoint of [
|
|
18
|
+
controlSummary, renderSearchResults, renderTimeline, featureAccess,
|
|
19
|
+
LeaseStore, migrationPlan, normalizeRollout, admitRuntimeCommand, runtimeCommandDigest,
|
|
20
|
+
normalizeQuery, projectTimeline, searchCatalog
|
|
21
|
+
]) assert.equal(typeof entrypoint, "function");
|
|
22
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Resolve the crash/reconnect timeline issue across
|
|
2
|
+
`packages/timeline/src/project.js`, `packages/timeline/src/checkpoint.js`, and
|
|
3
|
+
`apps/web/src/timeline-view.js`. The projection must remain deterministic when
|
|
4
|
+
events arrive out of order, repeat after reconnect, or stop at a capacity
|
|
5
|
+
boundary.
|
|
6
|
+
|
|
7
|
+
- [C1] `normalizeTimelineSnapshot(snapshot)` validates and returns a deep fresh
|
|
8
|
+
`{ cursor, messages, seen }`. Cursor is a non-negative safe integer; messages
|
|
9
|
+
have unique string ids containing at least one non-whitespace character,
|
|
10
|
+
string text, and boolean `complete`; `seen` is an object mapping event ids
|
|
11
|
+
containing at least one non-whitespace character to lowercase SHA-256 hex digests.
|
|
12
|
+
`projectTimeline(snapshot, events, { maxChars })` requires an event array and
|
|
13
|
+
a non-negative safe integer budget. Event `id` and `messageId` must be strings
|
|
14
|
+
containing at least one non-whitespace character; events also require a
|
|
15
|
+
positive safe `cursor`, non-negative safe `offset`, string text, and boolean
|
|
16
|
+
`complete`. Invalid input throws `TypeError` before returning.
|
|
17
|
+
- [C2] Event identity is SHA-256 over JSON with fields in this exact order:
|
|
18
|
+
`id`, `cursor`, `messageId`, `offset`, `text`, `complete`. Identical duplicate
|
|
19
|
+
ids are replay evidence; the same id with different content or different
|
|
20
|
+
events at one cursor throw `TypeError`. A historical event
|
|
21
|
+
(`cursor <= snapshot.cursor`) is accepted only when `seen[id]` equals its
|
|
22
|
+
digest; otherwise throw. Report replay ids once in first-observed order.
|
|
23
|
+
- [C3] Sort unique future events by cursor and apply only a contiguous prefix
|
|
24
|
+
beginning at `snapshot.cursor + 1`. At the first gap, stop without applying
|
|
25
|
+
later events, set `{ expected, observed }`, and buffer that event plus the
|
|
26
|
+
remainder. Create a new message on first use, preserve message order, require
|
|
27
|
+
`offset === current text length`, and reject appends to a complete message.
|
|
28
|
+
- [C4] `maxChars` limits total text across all projected messages. Equality is
|
|
29
|
+
allowed. Before the first event that would exceed it, stop atomically, leave
|
|
30
|
+
cursor/message/seen unchanged for that event, buffer it and the remainder,
|
|
31
|
+
and return `{ eventId, cursor, neededChars, maxChars }` as `backpressure`.
|
|
32
|
+
Return fresh `messages`, `seen`, `appliedIds`, `replayedIds`, and `buffered`
|
|
33
|
+
plus nullable `gap` and `backpressure`; never mutate inputs.
|
|
34
|
+
- [C5] `encodeTimelineCheckpoint(state)` revalidates the core snapshot and
|
|
35
|
+
returns JSON containing `{ schemaVersion: 1, payload, checksum }`, where the
|
|
36
|
+
checksum is SHA-256 of recursively key-sorted canonical JSON for payload.
|
|
37
|
+
`decodeTimelineCheckpoint(serialized)` imports and reuses
|
|
38
|
+
`normalizeTimelineSnapshot`, rejects malformed JSON, versions, structure, or
|
|
39
|
+
checksum with `TypeError`, and returns fresh data that cannot mutate a later
|
|
40
|
+
decode.
|
|
41
|
+
- [C6] `renderTimeline(messages)` requires an array and returns exactly one
|
|
42
|
+
ordered `<ol aria-label="Session timeline">`. Each message becomes
|
|
43
|
+
`<li data-id="..." data-state="complete|pending">...</li>`. Escape ids and
|
|
44
|
+
text for `&`, `<`, `>`, `"`, and `'`; empty input returns the exact empty list.
|
|
45
|
+
|
|
46
|
+
Preserve exports and add focused crash/replay/gap/budget/tamper tests. Change
|
|
47
|
+
only the declared source/test scope.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Resolve the control-plane correctness issue across
|
|
2
|
+
`packages/session/src/runtime-route.js`, `packages/session/src/admission.js`, and
|
|
3
|
+
`apps/web/src/control-view.js`. The browser must express exactly the same
|
|
4
|
+
bounded commands as Terminal while preserving durable revision and replay
|
|
5
|
+
semantics.
|
|
6
|
+
|
|
7
|
+
- [D1] `routeRuntimeCommand(input)` accepts only a non-null, non-array command
|
|
8
|
+
object whose fields are drawn from `idempotencyKey`, `expectedRevision`,
|
|
9
|
+
`kind`, `payload`, and `confirmed`. Supported kinds are `status`, `scout`,
|
|
10
|
+
`compact`, and `abort`. `payload` is always a non-null, non-array object.
|
|
11
|
+
`status`, `compact`, and `abort` require an empty payload. `scout` requires
|
|
12
|
+
exactly one string `objective` containing at least one non-whitespace
|
|
13
|
+
character, trimmed with internal whitespace collapsed. Any unknown kind,
|
|
14
|
+
field, payload field, or invalid value throws `TypeError`.
|
|
15
|
+
- [D2] Routing returns exactly `{ terminalCommand, confirmationRequired,
|
|
16
|
+
expectedModelCalls, effect }`. Routes are: `status` → `/status`, no
|
|
17
|
+
confirmation, `0`, `read`; `scout` → `/scout <normalized objective>`,
|
|
18
|
+
confirmation, `bounded`, `model`; `compact` → `/compact`, confirmation,
|
|
19
|
+
`bounded`, `semantic`; `abort` → `/abort`, confirmation, `0`, `state`.
|
|
20
|
+
`admitRuntimeCommand` must reject every confirmation-required command unless
|
|
21
|
+
`confirmed === true`.
|
|
22
|
+
- [D3] State is `{ revision, receipts }`, where revision is a non-negative safe
|
|
23
|
+
integer and receipts is an object. A new command requires an exact
|
|
24
|
+
non-negative safe `expectedRevision`. The idempotency key must be a string
|
|
25
|
+
containing at least one non-whitespace character, and the poison names
|
|
26
|
+
`__proto__`, `prototype`, and `constructor` are invalid.
|
|
27
|
+
Check a prior idempotency receipt before revision matching: an identical
|
|
28
|
+
replay succeeds even with a stale expected revision, returns the identical
|
|
29
|
+
state object, and marks only the returned receipt `replayed: true`. Reusing a
|
|
30
|
+
key for a different command throws `TypeError`.
|
|
31
|
+
- [D4] Export `runtimeCommandDigest(input)` from `admission.js`. It accepts
|
|
32
|
+
exactly `{ kind, payload }` and returns a SHA-256 digest of recursively
|
|
33
|
+
key-sorted canonical JSON. Canonicalization must reject non-finite or
|
|
34
|
+
non-JSON values. Equivalent object-key ordering must produce the same digest.
|
|
35
|
+
Admission binds idempotency to this digest for only the routed command's
|
|
36
|
+
`{ kind, payload }`.
|
|
37
|
+
- [D5] A newly admitted command increments revision exactly once and returns
|
|
38
|
+
`{ state, receipt }`. The stored receipt contains exactly `idempotencyKey`,
|
|
39
|
+
`commandDigest`, `kind`, `terminalCommand`, `effect`, `expectedModelCalls`,
|
|
40
|
+
`revisionBefore`, `revisionAfter`, and `replayed: false`. Do not mutate caller
|
|
41
|
+
state/input, and do not share the separately returned receipt object with the
|
|
42
|
+
stored receipt.
|
|
43
|
+
- [D6] `controlSummary(receipt)` rejects non-objects and returns exactly
|
|
44
|
+
`kind=<kind>; command=<JSON string>; revision=<before>-><after>; effect=<effect>; model=<0|bounded>; replayed=<true|false>`.
|
|
45
|
+
|
|
46
|
+
Preserve exports and add focused integration tests. Change only the declared
|
|
47
|
+
source/test scope.
|
package/benchmarks/capability-v1/references/causal-timeline-recovery/apps/web/src/timeline-view.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function escapeHtml(value) {
|
|
2
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function renderTimeline(messages) {
|
|
6
|
+
if (!Array.isArray(messages)) throw new TypeError("messages must be an array");
|
|
7
|
+
const rows = messages.map((item) => `<li data-id="${escapeHtml(item?.id ?? "")}" data-state="${item?.complete === true ? "complete" : "pending"}">${escapeHtml(item?.text ?? "")}</li>`).join("");
|
|
8
|
+
return `<ol aria-label="Session timeline">${rows}</ol>`;
|
|
9
|
+
}
|