diff --git a/capability-ledger.json b/capability-ledger.json new file mode 100644 index 00000000..e3dcab53 --- /dev/null +++ b/capability-ledger.json @@ -0,0 +1,79 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { + "capabilityId": "portable-identity", + "title": "Portable opaque identity contract", + "specification": "draft", + "implementation": "partial", + "repository": "draft-pr", + "verification": "fixture-proven", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "repo-path", "ref": "modules/jarvos-control-plane/src/identity.js" }, + { "type": "test", "ref": "modules/jarvos-control-plane/test/identity.test.js" }, + { "type": "document", "ref": "docs/architecture/capability-truth-ledger.md" }, + { "type": "pull-request", "ref": "pull/257" } + ], + "assertedOn": "2026-08-31", + "notes": "Draft-PR validator, exports, and unit tests exist; no product consumer issues identifiers yet." + }, + { + "capabilityId": "capability-truth-ledger", + "title": "Capability truth ledger contract", + "specification": "draft", + "implementation": "partial", + "repository": "draft-pr", + "verification": "fixture-proven", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "repo-path", "ref": "scripts/lib/capability-ledger.js" }, + { "type": "document", "ref": "docs/architecture/capability-truth-ledger.md" }, + { "type": "repo-path", "ref": "capability-ledger.json" }, + { "type": "pull-request", "ref": "pull/257" } + ], + "assertedOn": "2026-08-31", + "notes": "This draft-PR ledger validates against its own contract; no automated reporter consumes it yet." + }, + { + "capabilityId": "candidate-envelope", + "title": "Non-authoritative candidate envelope contract", + "specification": "draft", + "implementation": "partial", + "repository": "draft-pr", + "verification": "fixture-proven", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "repo-path", "ref": "modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js" }, + { "type": "test", "ref": "modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js" }, + { "type": "document", "ref": "modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md" }, + { "type": "test", "ref": "tests/fixtures/foundation-contracts/manifest.json" }, + { "type": "pull-request", "ref": "pull/257" } + ], + "assertedOn": "2026-08-31", + "notes": "Draft-PR validator, exports, unit tests, and shared conformance fixtures exist; no source adapter or promotion consumer constructs candidates yet." + }, + { + "capabilityId": "promotion-receipt", + "title": "Cross-surface promotion receipt envelope contract", + "specification": "draft", + "implementation": "partial", + "repository": "draft-pr", + "verification": "fixture-proven", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "repo-path", "ref": "modules/jarvos-control-plane/src/promotion-receipt.js" }, + { "type": "test", "ref": "modules/jarvos-control-plane/test/promotion-receipt.test.js" }, + { "type": "document", "ref": "modules/jarvos-control-plane/README.md" }, + { "type": "test", "ref": "tests/fixtures/foundation-contracts/manifest.json" }, + { "type": "pull-request", "ref": "pull/257" } + ], + "assertedOn": "2026-08-31", + "notes": "Draft-PR validator, exports, unit tests, and shared conformance fixtures exist; no destination writer emits this envelope yet." + } + ] +} diff --git a/docs/architecture/capability-truth-ledger.md b/docs/architecture/capability-truth-ledger.md new file mode 100644 index 00000000..ae81d867 --- /dev/null +++ b/docs/architecture/capability-truth-ledger.md @@ -0,0 +1,63 @@ +# Capability truth ledger + +The capability truth ledger is repository truth tooling. It records what the +project asserts about each capability and points to the evidence behind the +assertion. It is not discovery, not an activation probe, and not proof that a +capability runs anywhere. Reading the ledger tells you what has been claimed and +where to check it; validating it inspects no repository, host, process, or +network. In particular, a record on an open draft pull request is a draft +specification and `draft-pr` repository state, never canonical or +repository-shipped merely because its branch contains code and tests. + +## Schema + +A ledger is `{ schemaVersion, records }` with schema version +`jarvos.capability-ledger.v1`. Unknown schema versions, unknown enum values, and +unknown fields fail closed at both the top level and inside each record. + +## Independent truth dimensions + +Each record carries six independent dimensions. A capability can be strong on +one and absent on another, so the validator never derives one from another: + +| Dimension | States | +| --- | --- | +| `specification` | `absent`, `draft`, `canonical` | +| `implementation` | `absent`, `partial`, `complete` | +| `repository` | `local-only`, `draft-pr`, `merged`, `released` | +| `verification` | `untested`, `fixture-proven`, `clean-install-proven`, `live-canary-proven` | +| `activation` | `inactive`, `test-fixture`, `disposable`, `enrolled-host`, `production`, `unknown` | +| `authority` | `none`, `read-only`, `proposed`, `active`, `conflicted` | + +The activation vocabulary is generic public-product language. It never names a +specific operator's machines. + +## Records and evidence + +Every record also has a bounded lowercase `capabilityId`, a bounded `title`, an +ISO `assertedOn` date, optional bounded `notes`, and a non-empty `evidence` +array. Each evidence pointer is `{ type, ref }`, where type is `repo-path`, +`pull-request`, `test`, `document`, or `commit`. A reference is a bounded, +repository-relative pointer, except that a pull-request pointer is a bounded +tracker-relative reference such as `pull/257`; it cannot be absolute, cannot +contain `..` traversal, and cannot carry a `file:` or other URI scheme. + +## Invariants + +- Duplicate capability IDs fail. +- The six dimensions stay independent. +- Evidence references stay relative and scheme-free. +- The validator performs no I/O; it only checks shape and bounds. +- The ledger is an assertion set. A record is a claim with evidence pointers, + not a discovered fact and not an activation proof. + +## Seed ledger + +The tracked `capability-ledger.json` seed describes only the foundation work in +draft pull request 257. Its identity, truth-ledger, candidate-envelope, and +promotion-receipt contracts are asserted at `specification: draft`, +`implementation: partial`, and `verification: fixture-proven` because their +validators and tests exist but no installed product consumer activates them. +Every seed record is `repository: draft-pr`, `activation: inactive`, and +`authority: none`. Merge and release are separate future observations; neither +is inferred from this branch. diff --git a/docs/architecture/product-category-and-boundaries.md b/docs/architecture/product-category-and-boundaries.md index f3116612..a1325dd6 100644 --- a/docs/architecture/product-category-and-boundaries.md +++ b/docs/architecture/product-category-and-boundaries.md @@ -1,7 +1,7 @@ --- status: active created: 2026-05-17 -updated: 2026-05-17 +updated: 2026-08-31 canonical: true type: architecture --- @@ -43,6 +43,68 @@ The runtime owns model calls, shell execution, sandboxing, scheduling, messaging, and tool orchestration. jarvOS owns the user-controlled context and operating contract those runtimes hydrate from and write back to. +## Runtime modes and ownership + +jarvOS has no load-bearing harness. A compatible runtime is one optional way +to host a jarvOS interaction, never the permanent owner of the product's +identity, context, or governance contract. Runtime modes are intentionally +parallel: + +- **Native runtime mode:** a harness runs its own model, tools, scheduling, + delivery, and native session lifecycle. Native session identifiers and their + storage remain harness-owned. +- **jarvOS service mode:** jarvOS-owned services provide portable context, + policy, candidate, and receipt boundaries to compatible callers. Active + Assistant is a jarvOS-owned service boundary in this mode; it is not the + load-bearing owner of a particular harness. +- **Compatibility-read mode:** a harness may read compatible jarvOS context + through an adapter or documented file/API contract. The read direction is + from jarvOS-owned portable context to the harness; it does not import a + harness's private session state, require a harness to be installed, or grant + jarvOS ownership of native sessions. + +Portable conversation identity, explicit mappings from native sessions where a +harness safely exposes them, and cross-surface promotion receipts remain +jarvOS-owned contracts. A mapping is metadata, not a takeover: it cannot mint, +rename, replay, or infer a native session identity. + +Conformance is tiered so an adapter cannot borrow confidence from another +harness: contract/fixture conformance proves portable shape; adapter +conformance proves a named compatibility reader; and installed or live proof, +when a future adapter elects to provide it, proves only that named adapter. +The current foundation provides contract/fixture conformance only. It does not +claim adapter, installed, or live behavior. + +## Product Realization Boundary + +The product architecture permits a future adapter to observe eligible AI +interactions and prepare bounded, source-backed candidates for notes, journals, +memory, ontology, Projects, skills, or work. That future path could help +construct a useful digital twin across harnesses without treating every +transcript or model statement as true. This draft foundation implements no +source adapter, candidate storage, or promotion consumer. + +The boundary is explicit: + +- portable identities connect a mind, its installations, harness sessions, + source events, candidates, artifacts, policies, Projects, and receipts + without making one machine the permanent home of jarvOS; +- an asserted candidate is immutable and non-authoritative, contains evidence + pointers rather than raw source content, and cannot claim completion, + verification, destination state, or Project identity; +- promotion into authoritative memory, ontology, Projects, authored notes, or + other durable surfaces is governed by the owning writer and policy; +- a promotion receipt distinguishes a committed destination revision from an + already-satisfied, deferred, conflicted, or failed attempt and does not + promise universal rollback; and +- the capability truth ledger keeps specification, implementation, repository, + verification, activation, and authority state independent, so code or + documentation cannot masquerade as a live working capability. + +This is one mind across many harnesses, not one runtime controlling every +harness. Runtime-specific capabilities remain adapters behind portable jarvOS +contracts. + ## Guardrails - Runtime adapters are replaceable glue, not the product core. diff --git a/docs/architecture/secondbrain-external-integrations.md b/docs/architecture/secondbrain-external-integrations.md index 7ed48633..beea8f30 100644 --- a/docs/architecture/secondbrain-external-integrations.md +++ b/docs/architecture/secondbrain-external-integrations.md @@ -97,17 +97,36 @@ match. Missing or incompatible private fields produce `unknown`; the global ## Operating Model -The active secondbrain path is: +The intended secondbrain model is below. Only intentional capture is active in +this repository today; the eligible-interaction candidate stages are future +adapter behavior. ```text intentional capture -> Obsidian-compatible Markdown Notes/Journal + +future eligible AI interactions + -> eligibility and trust boundary + -> immutable, source-backed, non-authoritative candidates + -> governed review or policy admission + -> destination-owned promotion plus outcome receipt + +authoritative durable surfaces -> source-backed sidecars and generated LLM-wiki -> QMD freshness / broad lookup -> GBrain curated structured recall -> OpenClaw memory-wiki and runtime recall diagnostics ``` +Intentional capture remains the direct authored path. Eligible ambient +observation is a future proposal path: a future adapter may prepare candidates, +but this foundation contains no source adapter that does so. It does not +automatically believe, promote, or retain every conversation. Source +trust, privacy, expiry, deduplication, and evidence binding are checked before +candidate construction. Destination owners still decide admission and emit a +receipt whose outcome distinguishes a committed revision from a failed, +deferred, conflicted, or already-satisfied attempt. + Paperclip runs alongside that path for execution state. It records issues, owners, blockers, reviews, release evidence, and follow-up work, but it is not the knowledge base. @@ -147,7 +166,10 @@ Active integrations should be proven through these public-safe signals: ## Non-Goals -- No automatic ingestion of every AI conversation. +- No indiscriminate ingestion of every AI conversation, raw transcript + hoarding, or automatic belief. A future eligible-interaction adapter may + produce bounded non-authoritative candidates; durable promotion remains + governed. - No ChatGPT or Claude app first-class capture target in the current determinism contract. - No direct agentmemory host access or automatic memory promotion. diff --git a/docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md b/docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md new file mode 100644 index 00000000..f9047e8e --- /dev/null +++ b/docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md @@ -0,0 +1,537 @@ +# Product realization foundation v1 plan + +Status: implemented as a draft-PR contract slice; no runtime behavior is authorized + +Base: `af427a68bbe88c0f08dc516356e3fee55cf41b89` (`origin/main`) + +Branch: `feat/product-realization-foundation-v1` + +This plan is the integrated result of a read-only Fable 5/high first pass and a +Codex review against the exact base tree and current open pull-request file +sets. It implements the first safe slice from the parent Overseer roadmap. It +does not authorize a push, pull request, merge, release, installation, +activation, scheduler, runtime change, Vault operation, or private host +mutation. + +## Goal + +Add four fail-closed, machine-readable foundation contracts and the minimum +product-boundary documentation needed to describe them: + +1. a capability truth ledger that separates specification, implementation, + repository, verification, activation, and authority state; +2. opaque namespaced identities for portable jarvOS entities; +3. a non-authoritative candidate envelope bound to eligible source evidence; + and +4. a cross-surface promotion receipt envelope that reports outcome and + destination-specific reversibility honestly. + +These are contracts, fixtures, tests, and documentation only. Consumers and +stateful behavior are deliberately deferred. + +## Scope decision + +All four contract families belong in this slice because they refer to one +another and are the smallest coherent foundation for the governed candidate +lifecycle. The capability ledger records the truth about the contracts; +candidates and receipts reference stable identities; and receipts connect +non-authoritative candidates to a destination attempt without implementing the +attempt. + +Documentation reconciliation is limited to: + +- the product-boundary document; and +- the external-integration document's intentional-capture and non-goal + language. + +The broader README, strategy, packaging, onboarding, and public-baseline +rewrite remains separate. + +## Done criteria + +1. Each contract has a validator, exported constants, documentation, positive + fixtures, and negative fixtures. +2. Unknown schema versions, enum values, and top-level fields fail closed. +3. The candidate envelope can represent only eligible, non-authoritative + candidates and contains no mutable promotion status. +4. The receipt envelope distinguishes a recorded attempt from a successful + mutation and never implies universal rollback. +5. Project identities can be referenced but no generic function can mint them. +6. The seed capability ledger validates and describes this local branch + honestly: specification `draft`, repository `draft-pr`, activation `inactive`, authority `none`, + implementation `partial`, verification `fixture-proven`. +7. The cross-contract fixture suite runs through the existing control-plane + test glob, so root `package.json` is unchanged. +8. The exact-base test suite passes before and after implementation. +9. The final diff contains no writers, services, schedulers, network clients, + setup behavior, runtime activation, environment-derived identity, personal + data, or production evidence. +10. Work is committed locally in reviewable boundaries and is not pushed. + +## Constraints + +- Work only in the dedicated `jarvOS-product-foundation` worktree. +- Add no npm dependency. +- Use existing CommonJS validator conventions. +- Do not add a package merely to hold contracts. +- Do not change root `package.json`; open pull requests #252, #253, and #255 + already touch it. +- Do not touch bootstrap, CLI, Doctor, profiles, setup scripts, hooks, runtimes, + templates, schedulers, adapters, source-event producers, stores, protected + writers, or provider configuration. +- Do not implement identifier issuance. The identity contract validates opaque + identifiers; each owning subsystem will later define how it issues them. +- Do not derive identity from a hostname, filesystem path, username, MAC + address, environment variable, or other private/unstable machine property. +- Do not implement authority records, authority transfer, expiring leases, or + automatic failover. +- Do not put raw transcript text, Vault content, real paths, personal data, + secrets, or deployment receipts in code, fixtures, docs, or test output. +- Do not treat the capability ledger as discovery or proof. Its records are + assertions with evidence pointers. +- Do not let a candidate satisfy recall, completion, Project identity, or + authoritative memory. +- Do not claim rollback when the destination offers only supersession, + retraction, or no reversal. + +## Ownership decisions + +| Contract | Owner | Location | +| --- | --- | --- | +| Opaque identity | `@jarvos/control-plane` | `modules/jarvos-control-plane/src/identity.js` | +| Promotion receipt | `@jarvos/control-plane` | `modules/jarvos-control-plane/src/promotion-receipt.js` | +| Candidate envelope | `@jarvos/ambient` | `modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js` | +| Capability ledger | Repository truth tooling | `scripts/lib/capability-ledger.js` and root `capability-ledger.json` | +| Cross-contract conformance | Control-plane test lane | `modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js` | + +The control plane already owns shared governance vocabulary. Ambient already +owns CaptureEvent. Repository release tooling already lives under +`scripts/lib`. Existing destination receipts remain authoritative for their +own evidence; the new receipt is a bounded cross-surface envelope, not a +replacement. + +## Identity contract + +Schema version: `jarvos.identity.v1`. + +Identifier grammar: + +```text +jarvos::: +``` + +Requirements: + +- the entire identifier is lowercase and no more than 256 characters; +- `namespace` and `opaque` each match + `[a-z0-9][a-z0-9._-]{0,63}`; +- no segment contains whitespace, control characters, path separators, + percent-encoding, or an additional colon; +- the allowed kinds are `mind`, `installation`, `host`, `harness-instance`, + `session`, `source-event`, `candidate`, `artifact`, `project`, `policy`, and + `receipt`; +- an optional expected-kind argument must match exactly; +- parsing and validation never resolve, dereference, normalize, or infer an + identifier; and +- no minting API is included in v1. + +The owning issuer controls stability and idempotency. In particular, the +Projects provider owns `project` issuance, source adapters own source-event and +session mapping, and a future enrollment flow owns mind, installation, host, +and harness-instance issuance. + +Exports: + +- `IDENTITY_SCHEMA_VERSION` +- `IDENTITY_KINDS` +- `parseIdentity(value)` +- `validateIdentity(value, expectedKind)` returning an error array +- `assertIdentity(value, expectedKind)` throwing on invalid input + +## Capability ledger contract + +Schema version: `jarvos.capability-ledger.v1`. + +Ledger shape: + +```text +{ schemaVersion, records } +``` + +Every record contains: + +- `capabilityId`: bounded lowercase slug; +- `title`: bounded non-empty string; +- `specification`: `absent`, `draft`, or `canonical`; +- `implementation`: `absent`, `partial`, or `complete`; +- `repository`: `local-only`, `draft-pr`, `merged`, or `released`; +- `verification`: `untested`, `fixture-proven`, `clean-install-proven`, or + `live-canary-proven`; +- `activation`: `inactive`, `test-fixture`, `disposable`, `enrolled-host`, + `production`, or `unknown`; +- `authority`: `none`, `read-only`, `proposed`, `active`, or `conflicted`; +- `evidence`: a non-empty array of bounded `{ type, ref }` pointers, where type + is `repo-path`, `pull-request`, `test`, `document`, or `commit`; +- `assertedOn`: an ISO calendar date; and +- optional bounded `notes`. + +The activation vocabulary is generic public-product language. It must not name +an operator's machines. + +Invariants: + +- duplicate capability IDs fail; +- all six truth dimensions remain independent; +- an evidence reference cannot be absolute, contain traversal, or contain a + `file:` URI; +- the validator does not inspect a repository, host, process, or network; +- the ledger is an assertion set, not discovery and not activation proof; and +- the seed contains only the four contracts in this slice. + +The seed records use `implementation: partial` because validators and fixtures +exist but no product consumer does. While this work remains in a draft pull +request, specification is `draft`, verification is `fixture-proven`, repository +is `draft-pr`, activation is `inactive`, and authority is `none`. + +## Candidate envelope contract + +Schema version: `jarvos.candidate.v1`. + +`assertCandidate()` returns an immutable deep clone. The envelope has no +mutable `status` and cannot itself record promotion, rejection, completion, or +recall. + +Required fields: + +- `schemaVersion`; +- `candidateId`, kind `candidate`; +- `candidateType`: `note-draft`, `journal-suggestion`, `memory-unit`, + `ontology-inquiry`, `project-signal`, `skill-proposal`, or `work-proposal`; +- `authority`: required literal `non-authoritative`; +- `sources`: a non-empty array of `{ sourceEventId, evidenceDigest }`, where + the event ID has kind `source-event` and the digest is `sha256:<64 hex>`; +- `privacyTier`: `public`, `local-private`, `private`, or `sensitive`; +- `sourceTrust`: `user-authored` or `assistant-derived`; +- `construction`: `{ extractorId, extractorVersion, eligibilityPolicyId }`, + with a policy-kind identifier; +- `dedupeKey`: bounded opaque string; +- `createdAt` and `expiresAt`: ISO timestamps, with expiry after creation; and +- `proposal`: bounded `{ title, summary }` containing no raw transcript field. + +Invariants: + +- `secret`, untrusted recall, tool output, unknown ownership, and unknown trust + are not candidate values; they are rejected before candidate construction; +- source-event IDs cannot repeat; +- candidates contain evidence digests and source identity, not raw evidence; +- fields such as `authoritative`, `verified`, `completed`, `status`, + `destination`, and `recallText` are unknown and rejected; +- the module exports no recall, completion, write, store, or promotion + function; and +- `project-signal` may reference source evidence but cannot mint or replace a + Project identity. + +## Promotion receipt envelope contract + +Schema version: `jarvos.promotion-receipt.v1`. + +Required fields: + +- `schemaVersion`; +- `receiptId`, kind `receipt`; +- `operation`: `promotion`, `supersession`, `retraction`, `rollback`, or + `correction`; +- `outcome`: `committed`, `already_satisfied`, `deferred`, `conflict`, or + `failed`; +- `candidateIds`: candidate-kind identifiers; +- `policyId`: policy-kind identifier; +- `authorization`: `{ mode }`, where mode is `user-reviewed` or + `policy-automatic`; +- `destination`: `{ surface, artifactId, revisionBefore, revisionAfter, + reversalMode }`; +- `recordedAt`: ISO timestamp; and +- `evidence`: a non-empty array of `{ type, ref, digest }`, using bounded + logical references and `sha256:<64 hex>` digests. + +Destination surfaces are `notes`, `journal`, `memory`, `ontology`, `projects`, +`skills`, and `work`. `artifactId` is an artifact-kind identity. +`reversalMode` is `rollback`, `retraction`, `supersession`, or `none`. + +Optional `predecessorReceiptId` is required for supersession, retraction, +rollback, and correction. It has kind `receipt`. + +Invariants: + +- promotion requires at least one candidate ID; +- non-promotion operations require a predecessor receipt; +- `committed` requires `revisionAfter`; +- `deferred`, `conflict`, and `failed` cannot claim `revisionAfter`; +- `already_satisfied` reports no new revision and cannot claim mutation; +- `reversalMode` describes how the **resulting destination state may later be + changed**; it does not prove that the current operation was reversible or + that a future reversal will succeed; +- the validator does not try to prove destination-specific reversal semantics; + the destination's own receipt and protected writer retain that ownership; +- `policy-automatic` is representable but does not authorize automation; an + admitting consumer must enforce the currently approved policy posture; +- raw destination receipts, content, paths, secrets, and transcript text are + not included; and +- a receipt with a failed or deferred outcome is evidence of an attempt, not + evidence of a successful promotion. + +## Fixture matrix + +Fixtures live under `tests/fixtures/foundation-contracts/` with a manifest that +names the contract, file, and expected outcome. + +Positive cases: + +- every identity kind; +- a multi-record generic capability ledger; +- a `memory-unit` candidate; +- a `project-signal` candidate with no minted Project identity; +- committed and failed promotion receipts; +- a supersession receipt with a predecessor; and +- a committed destination whose future reversal mode is `none`. + +Negative cases: + +- unknown schema version, enum value, identity kind, or top-level field for + every contract; +- uppercase, path-like, percent-encoded, overlong, and extra-colon identities; +- duplicate ledger capability IDs, missing evidence, absolute/traversal/file + evidence references, and a machine-specific activation value; +- authoritative candidate, candidate `status`, empty or duplicate sources, + bad digest, `secret` privacy, untrusted/tool/unknown trust, missing expiry, + expiry before creation, and raw transcript/content field; +- promotion without a candidate, non-promotion without a predecessor, + committed receipt without `revisionAfter`, non-committed receipt with + `revisionAfter`, unknown destination surface/outcome/reversal, and malformed + candidate, policy, artifact, or receipt identity. + +All fixture values are synthetic and generic. + +## Exact files + +Add: + +- `docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md` +- `modules/jarvos-control-plane/src/identity.js` +- `modules/jarvos-control-plane/src/promotion-receipt.js` +- `modules/jarvos-control-plane/test/identity.test.js` +- `modules/jarvos-control-plane/test/promotion-receipt.test.js` +- `modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js` +- `modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js` +- `modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js` +- `modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md` +- `scripts/lib/capability-ledger.js` +- `capability-ledger.json` +- `docs/architecture/capability-truth-ledger.md` +- `tests/fixtures/foundation-contracts/manifest.json` +- bounded valid and invalid JSON fixtures named by the manifest. + +Change additively: + +- `modules/jarvos-control-plane/src/index.js` +- `modules/jarvos-control-plane/package.json` +- `modules/jarvos-control-plane/README.md` +- `modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/index.js` +- `modules/jarvos-secondbrain/packages/jarvos-ambient/package.json` +- `docs/architecture/product-category-and-boundaries.md` +- `docs/architecture/secondbrain-external-integrations.md` +- `tests/secondbrain-external-integrations-doc-test.js` only as required to + enforce the reconciled wording. + +Do not change root `package.json`. + +## Ordered steps and verification + +### Step 1 — Baseline and plan commit + +1. Verify branch, worktree, and exact base. +2. Install locked dependencies only inside this worktree with `npm ci`. +3. Run `npm test` at the exact base. +4. If baseline is red, stop; do not fix unrelated behavior. +5. Commit this plan artifact before implementation. + +Verification: + +```text +git rev-parse HEAD +git status --short --branch +npm test +``` + +### Step 2 — Identity contract + +Implement the identity validator, exports, module tests, and README section. +Do not include minting, machine discovery, or environment access. + +Verification: + +```text +node --test modules/jarvos-control-plane/test/identity.test.js +node --test modules/jarvos-control-plane/test/*.test.js +``` + +### Step 3 — Capability ledger + +Implement the ledger validator, generic contract documentation, and four-record +seed ledger. + +Verification: + +```text +node -e "require('./scripts/lib/capability-ledger').assertCapabilityLedger(require('./capability-ledger.json'))" +``` + +Also prove one malformed in-memory ledger is rejected. + +### Step 4 — Candidate envelope + +Implement the candidate validator, immutable assertion result, ambient exports, +tests, and contract documentation. + +Verification: + +```text +node --test modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js +npm test --prefix modules/jarvos-secondbrain/packages/jarvos-ambient +``` + +### Step 5 — Promotion receipt envelope + +Implement the receipt validator, exports, tests, and README section. + +Verification: + +```text +node --test modules/jarvos-control-plane/test/promotion-receipt.test.js +node --test modules/jarvos-control-plane/test/*.test.js +``` + +### Step 6 — Fixture conformance + +Add the manifest and JSON fixtures. The control-plane conformance test must +load each fixture, dispatch it to the selected validator, and match the expected +result. It must also validate the tracked seed ledger. + +Verification: + +```text +node --test modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js +node --test modules/jarvos-control-plane/test/*.test.js +``` + +### Step 7 — Product-boundary documentation reconciliation + +Add a bounded product-boundary section for candidates, receipts, identities, +and truth states. Amend the external-integration operating model and non-goal so +they distinguish intentional capture from eligible ambient candidate creation +and still reject indiscriminate conversation ingestion or automatic belief. + +Verification: + +```text +node --test tests/secondbrain-external-integrations-doc-test.js +npm run test:structure +``` + +### Step 8 — Full review and verification + +Run the complete suite, inspect the exact diff, verify forbidden paths and +imports, and confirm the branch has no upstream and no remote ref. + +Verification: + +```text +npm test +git diff --check af427a68bbe88c0f08dc516356e3fee55cf41b89..HEAD +git status --short --branch +git log --oneline af427a68bbe88c0f08dc516356e3fee55cf41b89..HEAD +``` + +## Failure modes + +| Failure | Response | +| --- | --- | +| Baseline suite fails | Stop and report; do not absorb unrelated fixes | +| Existing owner or export convention differs | Follow the exact-base owner; do not create a new package | +| Open PR overlaps an implementation file | Drop or relocate the overlapping additive edit; do not rebase another PR | +| Validator accepts unknown version, enum, or field | Blocking defect; add or fix a rejecting test | +| Candidate schema can carry ineligible or authoritative state | Blocking defect; narrow the schema | +| Receipt implies successful mutation for a non-committed outcome | Blocking defect; enforce outcome/revision invariant | +| Receipt implies universal rollback | Blocking defect; clarify reversal mode and destination ownership | +| Fixture contains real/private information | Replace it before commit and inspect history | +| Implementation requires store, CLI, Doctor, runtime, or setup changes | Defer to the next slice | +| Full suite reveals an unrelated regression | Confirm base behavior; stop rather than broaden scope | + +## Pull-request overlap strategy + +The current relevant draft pull requests touch onboarding, Obsidian probes, +Codex rollback, runtime-kit, and root `package.json`. Read-only file inspection +found no overlap with the new contract source files. This plan deliberately +avoids root `package.json` to eliminate the known shared file. + +Do not rebase or modify pull requests #249, #252, #253, #255, or #256. Before +final review, repeat their changed-file inspection. If new overlap appears, +keep this branch local and report the integration order rather than rewriting +the other branches. + +## Commit boundaries + +1. `docs: add product realization foundation plan` +2. `feat(control-plane): add portable identity contract` +3. `feat: add capability truth ledger contract` +4. `feat(ambient): add candidate envelope contract` +5. `feat(control-plane): add promotion receipt contract` +6. `test: add foundation contract conformance fixtures` +7. `docs: reconcile candidate and truth boundaries` +8. A correction commit only if review finds a defect; otherwise none. + +## Review gates + +1. The plan artifact is committed before implementation. +2. Opus implements only this plan and reports every divergence. +3. Codex reviews the exact diff against every field-level invariant and runs + focused plus full tests. +4. A fresh independent model review examines the final local commit for + contract errors, privacy leakage, scope expansion, and regressions. +5. Findings are fixed and the full suite is rerun before completion. +6. Push, pull request, merge, release, install, and activation remain separate + user decisions. + +## Deferred work + +- full capability audit and automatic ledger reporter; +- Doctor, release, and CI consumption of the ledger; +- identity enrollment, persistence, recovery, and authority transfer; +- candidate store, expiry worker, review queue, policy engine, deduplication, + conflict resolution, and redacted projections; +- source adapters that construct candidates; +- destination executors and destination-specific receipt embedding; +- correction, forgetting, and derived-layer rebuild implementation; +- session pointers and context stamps; +- onboarding, profile, runtime, OpenClaw, multi-machine, and Mini work; and +- all automatic promotion. + +## No-production-mutation proof checklist + +- [ ] Branch is based on the exact recorded `origin/main` commit. +- [ ] Branch has no upstream and no remote branch. +- [ ] No push or pull request was created. +- [ ] Diff touches only the listed contract, fixture, test, and documentation + files. +- [ ] Root `package.json`, lockfile, bootstrap, CLI, Doctor, profiles, runtimes, + templates, hooks, and setup paths are unchanged. +- [ ] No code imports network, scheduler, OS identity, environment-derived + identity, Vault, Obsidian, GBrain, or Paperclip behavior. +- [ ] No fixture or document contains personal data, real absolute paths, + secrets, transcript text, Vault content, or deployment evidence. +- [ ] Seed ledger says `draft`, `draft-pr`, `inactive`, `none`, `partial`, and + `fixture-proven`. +- [ ] Candidates are structurally non-authoritative and immutable. +- [ ] Receipts distinguish attempts from successful mutation and do not promise + rollback. +- [ ] Focused and full test suites pass. +- [ ] Git status is clean after local commits. diff --git a/modules/jarvos-control-plane/README.md b/modules/jarvos-control-plane/README.md index 94f88878..74c1ca8b 100644 --- a/modules/jarvos-control-plane/README.md +++ b/modules/jarvos-control-plane/README.md @@ -41,6 +41,46 @@ Paperclip, OpenClaw, Codex, Claude Code, cron, or a private machine path. - It is not permission to mutate by free-form text. Requests must declare a resource, mutation class, authority, command spec, budget, and lifecycle. +## Portable identity + +The package validates opaque `jarvos.identity.v1` identifiers so candidates, +receipts, sessions, and other portable entities can reference stable identities +without a shared database. An identifier has the fixed grammar + +```text +jarvos::: +``` + +The whole value is lowercase and at most 256 characters. `namespace` and +`opaque` each match `[a-z0-9][a-z0-9._-]{0,63}` and contain no whitespace, +path separator, percent-encoding, or extra colon. The allowed kinds are `mind`, +`installation`, `host`, `harness-instance`, `session`, `source-event`, +`candidate`, `artifact`, `project`, `policy`, and `receipt`. + +`validateIdentity(value, expectedKind)` returns an error array, `parseIdentity` +returns the four bounded segments, and `assertIdentity` throws on invalid input. +Validation never resolves, dereferences, normalizes, or infers an identifier, +and it reads no host, process, or environment state. This contract does not mint +identifiers; each owning subsystem defines how it issues them. + +## Promotion receipts + +The package also validates `jarvos.promotion-receipt.v1` envelopes. A receipt +records the outcome of a governed promotion, supersession, retraction, +rollback, or correction across the notes, journal, memory, ontology, Projects, +skills, and work surfaces. It binds portable candidate, policy, artifact, and +receipt identities to bounded evidence digests without embedding raw content, +destination receipts, secrets, or host paths. + +A `committed` outcome names the resulting destination revision. Every other +outcome reports no new revision and no reversal claim; in particular, a failed +or deferred receipt proves only that an attempt was recorded. `reversalMode` +describes how committed destination state may later be changed. It does not +prove that a reversal is available or will succeed. Destination-specific +writers and receipts retain that responsibility. Likewise, +`policy-automatic` is representable as provenance but does not itself grant +automation authority. + ## Public human and agent adapters The package ships `jarvos-manager` for a human CLI and exposes the same diff --git a/modules/jarvos-control-plane/package.json b/modules/jarvos-control-plane/package.json index b151bab3..e603f9c5 100644 --- a/modules/jarvos-control-plane/package.json +++ b/modules/jarvos-control-plane/package.json @@ -13,7 +13,9 @@ "./storage": "./src/storage/index.js", "./application-service": "./src/application-service.js", "./manager": "./scripts/jarvos-manager.js", - "./protected-resource": "./src/protected-resource/index.js" + "./protected-resource": "./src/protected-resource/index.js", + "./identity": "./src/identity.js", + "./promotion-receipt": "./src/promotion-receipt.js" }, "bin": { "jarvos-manager": "scripts/jarvos-manager.js" diff --git a/modules/jarvos-control-plane/src/identity.js b/modules/jarvos-control-plane/src/identity.js new file mode 100644 index 00000000..e9ff706e --- /dev/null +++ b/modules/jarvos-control-plane/src/identity.js @@ -0,0 +1,95 @@ +'use strict'; + +// Portable, opaque identity contract for jarvOS entities. This module only +// checks the shape of an already-issued identifier. It never mints, resolves, +// dereferences, normalizes, or infers an identity, and it reads no host, +// process, environment, or network state. Each owning subsystem defines its own +// issuance: the Projects provider owns `project` identifiers, source adapters +// own source-event and session mapping, and a future enrollment flow owns mind, +// installation, host, and harness-instance issuance. + +const IDENTITY_SCHEMA_VERSION = 'jarvos.identity.v1'; +const IDENTITY_SCHEME = 'jarvos'; +const MAX_IDENTITY_LENGTH = 256; + +const IDENTITY_KINDS = Object.freeze([ + 'mind', + 'installation', + 'host', + 'harness-instance', + 'session', + 'source-event', + 'candidate', + 'artifact', + 'project', + 'policy', + 'receipt', +]); + +const IDENTITY_KIND_SET = new Set(IDENTITY_KINDS); +const SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/; + +function validateIdentity(value, expectedKind) { + const errors = []; + if (expectedKind !== undefined && !IDENTITY_KIND_SET.has(expectedKind)) { + errors.push('expected kind is not a known identity kind'); + return errors; + } + if (typeof value !== 'string') { + errors.push('identity must be a string'); + return errors; + } + if (value.length === 0 || value.length > MAX_IDENTITY_LENGTH) { + errors.push('identity length is out of range'); + return errors; + } + if (value !== value.toLowerCase()) { + errors.push('identity must be lowercase'); + } + const segments = value.split(':'); + if (segments.length !== 4) { + errors.push('identity must have exactly four colon-delimited segments'); + return errors; + } + const [scheme, kind, namespace, opaque] = segments; + if (scheme !== IDENTITY_SCHEME) { + errors.push('identity scheme must be "jarvos"'); + } + if (!IDENTITY_KIND_SET.has(kind)) { + errors.push('identity kind is not a known kind'); + } + if (!SEGMENT_PATTERN.test(namespace)) { + errors.push('identity namespace is malformed'); + } + if (!SEGMENT_PATTERN.test(opaque)) { + errors.push('identity opaque segment is malformed'); + } + if (expectedKind !== undefined && kind !== expectedKind) { + errors.push('identity kind does not match the expected kind'); + } + return errors; +} + +function parseIdentity(value, expectedKind) { + if (validateIdentity(value, expectedKind).length > 0) { + return null; + } + const [scheme, kind, namespace, opaque] = value.split(':'); + return { scheme, kind, namespace, opaque }; +} + +function assertIdentity(value, expectedKind) { + const errors = validateIdentity(value, expectedKind); + if (errors.length > 0) { + throw new TypeError(`invalid jarvos identity: ${errors.join('; ')}`); + } + return value; +} + +module.exports = { + IDENTITY_SCHEMA_VERSION, + IDENTITY_KINDS, + parseIdentity, + validateIdentity, + assertIdentity, +}; diff --git a/modules/jarvos-control-plane/src/index.js b/modules/jarvos-control-plane/src/index.js index 3df33d6b..044c0a3a 100644 --- a/modules/jarvos-control-plane/src/index.js +++ b/modules/jarvos-control-plane/src/index.js @@ -8,6 +8,8 @@ const storage = require('./storage'); const applicationService = require('./application-service'); const protectedResource = require('./protected-resource'); const schedulerAdapter = require('./scheduler-adapter'); +const identity = require('./identity'); +const promotionReceipt = require('./promotion-receipt'); module.exports = { ...contracts, @@ -18,4 +20,6 @@ module.exports = { ...applicationService, ...protectedResource, ...schedulerAdapter, + ...identity, + ...promotionReceipt, }; diff --git a/modules/jarvos-control-plane/src/promotion-receipt.js b/modules/jarvos-control-plane/src/promotion-receipt.js new file mode 100644 index 00000000..59cccfe0 --- /dev/null +++ b/modules/jarvos-control-plane/src/promotion-receipt.js @@ -0,0 +1,244 @@ +'use strict'; + +// Bounded, cross-surface evidence that a governed promotion operation was +// attempted. This envelope reports an outcome; it does not authorize a write, +// prove destination-specific semantics, or embed raw content or host paths. + +const { validateIdentity } = require('./identity'); + +const PROMOTION_RECEIPT_SCHEMA_VERSION = 'jarvos.promotion-receipt.v1'; +const PROMOTION_RECEIPT_OPERATIONS = Object.freeze([ + 'promotion', 'supersession', 'retraction', 'rollback', 'correction', +]); +const PROMOTION_RECEIPT_OUTCOMES = Object.freeze([ + 'committed', 'already_satisfied', 'deferred', 'conflict', 'failed', +]); +const PROMOTION_AUTHORIZATION_MODES = Object.freeze([ + 'user-reviewed', 'policy-automatic', +]); +const PROMOTION_DESTINATION_SURFACES = Object.freeze([ + 'notes', 'journal', 'memory', 'ontology', 'projects', 'skills', 'work', +]); +const PROMOTION_REVERSAL_MODES = Object.freeze([ + 'rollback', 'retraction', 'supersession', 'none', +]); +const PROMOTION_EVIDENCE_TYPES = Object.freeze([ + 'destination-receipt', 'policy-decision', 'authorization', 'verification', +]); + +const RECEIPT_KEYS = [ + 'schemaVersion', 'receiptId', 'operation', 'outcome', 'candidateIds', + 'policyId', 'authorization', 'destination', 'recordedAt', 'evidence', +]; +const AUTHORIZATION_KEYS = ['mode']; +const DESTINATION_KEYS = [ + 'surface', 'artifactId', 'revisionBefore', 'revisionAfter', 'reversalMode', +]; +const EVIDENCE_KEYS = ['type', 'ref', 'digest']; +const MAX_CANDIDATES = 64; +const MAX_EVIDENCE = 64; +const MAX_LOGICAL_REF_LENGTH = 256; +const MAX_REVISION_LENGTH = 256; +const LOGICAL_REF_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/; +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/; +const UTC_INSTANT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?Z$/; + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function hasExactKeys(value, keys) { + return isPlainObject(value) + && Object.keys(value).every((key) => keys.includes(key)) + && keys.every((key) => Object.hasOwn(value, key)); +} + +function isBoundedString(value, max) { + return typeof value === 'string' && value.length > 0 && value.length <= max; +} + +function isLogicalRef(value) { + return isBoundedString(value, MAX_LOGICAL_REF_LENGTH) + && LOGICAL_REF_PATTERN.test(value) + && !value.includes('..') + && !value.startsWith('file:'); +} + +function daysInMonth(year, month) { + if (month === 2) { + const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + return leap ? 29 : 28; + } + return [4, 6, 9, 11].includes(month) ? 30 : 31; +} + +function isRealUtcInstant(value) { + if (typeof value !== 'string') return false; + const match = UTC_INSTANT_PATTERN.exec(value); + if (!match) return false; + const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match; + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + const hour = Number(hourText); + const minute = Number(minuteText); + const second = Number(secondText); + if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59) return false; + if (day > daysInMonth(year, month)) return false; + return !Number.isNaN(Date.parse(value)); +} + +function validateAuthorization(authorization, errors) { + if (!hasExactKeys(authorization, AUTHORIZATION_KEYS)) { + errors.push('authorization must contain only mode'); + return; + } + if (!PROMOTION_AUTHORIZATION_MODES.includes(authorization.mode)) { + errors.push('authorization has an unknown mode'); + } +} + +function validateDestination(destination, outcome, errors) { + if (!hasExactKeys(destination, DESTINATION_KEYS)) { + errors.push('destination must contain only surface, artifactId, revisionBefore, revisionAfter, and reversalMode'); + return; + } + if (!PROMOTION_DESTINATION_SURFACES.includes(destination.surface)) { + errors.push('destination has an unknown surface'); + } + if (validateIdentity(destination.artifactId, 'artifact').length > 0) { + errors.push('destination artifactId must be an artifact identity'); + } + if (destination.revisionBefore !== null + && (!isBoundedString(destination.revisionBefore, MAX_REVISION_LENGTH) + || !isLogicalRef(destination.revisionBefore))) { + errors.push('destination revisionBefore must be null or a bounded logical reference'); + } + if (!PROMOTION_REVERSAL_MODES.includes(destination.reversalMode)) { + errors.push('destination has an unknown reversalMode'); + } + if (outcome === 'committed') { + if (!isBoundedString(destination.revisionAfter, MAX_REVISION_LENGTH) + || !isLogicalRef(destination.revisionAfter)) { + errors.push('a committed receipt requires a bounded logical revisionAfter'); + } + } else { + if (destination.revisionAfter !== null) { + errors.push('a non-committed receipt must set revisionAfter to null'); + } + if (destination.reversalMode !== 'none') { + errors.push('a non-committed receipt must set reversalMode to none'); + } + } +} + +function validateEvidence(evidence, index, errors) { + const label = `evidence[${index}]`; + if (!hasExactKeys(evidence, EVIDENCE_KEYS)) { + errors.push(`${label} must contain only type, ref, and digest`); + return; + } + if (!PROMOTION_EVIDENCE_TYPES.includes(evidence.type)) { + errors.push(`${label} has an unknown type`); + } + if (!isLogicalRef(evidence.ref)) { + errors.push(`${label} ref must be a bounded logical reference`); + } + if (typeof evidence.digest !== 'string' || !DIGEST_PATTERN.test(evidence.digest)) { + errors.push(`${label} digest must be sha256:<64 hex>`); + } +} + +function validatePromotionReceipt(receipt) { + const errors = []; + if (!isPlainObject(receipt)) { + errors.push('promotion receipt must be an object'); + return errors; + } + const allowedKeys = [...RECEIPT_KEYS, 'predecessorReceiptId']; + if (!Object.keys(receipt).every((key) => allowedKeys.includes(key))) { + errors.push('promotion receipt contains an unknown field'); + } + for (const key of RECEIPT_KEYS) { + if (!Object.hasOwn(receipt, key)) errors.push(`promotion receipt is missing ${key}`); + } + + if (receipt.schemaVersion !== PROMOTION_RECEIPT_SCHEMA_VERSION) { + errors.push('promotion receipt schemaVersion is unsupported'); + } + if (validateIdentity(receipt.receiptId, 'receipt').length > 0) { + errors.push('receiptId must be a receipt identity'); + } + if (!PROMOTION_RECEIPT_OPERATIONS.includes(receipt.operation)) { + errors.push('promotion receipt has an unknown operation'); + } + if (!PROMOTION_RECEIPT_OUTCOMES.includes(receipt.outcome)) { + errors.push('promotion receipt has an unknown outcome'); + } + + if (!Array.isArray(receipt.candidateIds) || receipt.candidateIds.length > MAX_CANDIDATES) { + errors.push('candidateIds must be a bounded array'); + } else { + const seen = new Set(); + for (let index = 0; index < receipt.candidateIds.length; index += 1) { + const candidateId = receipt.candidateIds[index]; + if (validateIdentity(candidateId, 'candidate').length > 0) { + errors.push(`candidateIds[${index}] must be a candidate identity`); + } else if (seen.has(candidateId)) { + errors.push(`candidateIds[${index}] duplicates an earlier candidate identity`); + } else { + seen.add(candidateId); + } + } + } + if (receipt.operation === 'promotion') { + if (!Array.isArray(receipt.candidateIds) || receipt.candidateIds.length === 0) { + errors.push('promotion requires at least one candidateId'); + } + if (Object.hasOwn(receipt, 'predecessorReceiptId')) { + errors.push('promotion cannot declare a predecessorReceiptId'); + } + } else if (PROMOTION_RECEIPT_OPERATIONS.includes(receipt.operation)) { + if (!Object.hasOwn(receipt, 'predecessorReceiptId') + || validateIdentity(receipt.predecessorReceiptId, 'receipt').length > 0) { + errors.push('non-promotion operation requires a receipt predecessorReceiptId'); + } + } + + if (validateIdentity(receipt.policyId, 'policy').length > 0) { + errors.push('policyId must be a policy identity'); + } + validateAuthorization(receipt.authorization, errors); + validateDestination(receipt.destination, receipt.outcome, errors); + if (!isRealUtcInstant(receipt.recordedAt)) { + errors.push('recordedAt must be a real ISO UTC instant'); + } + if (!Array.isArray(receipt.evidence) || receipt.evidence.length === 0 || receipt.evidence.length > MAX_EVIDENCE) { + errors.push('evidence must be a bounded non-empty array'); + } else { + for (let index = 0; index < receipt.evidence.length; index += 1) { + validateEvidence(receipt.evidence[index], index, errors); + } + } + return errors; +} + +function assertPromotionReceipt(receipt) { + const errors = validatePromotionReceipt(receipt); + if (errors.length > 0) { + throw new Error(`invalid jarvos promotion receipt: ${errors.join('; ')}`); + } + return receipt; +} + +module.exports = { + PROMOTION_RECEIPT_SCHEMA_VERSION, + PROMOTION_RECEIPT_OPERATIONS, + PROMOTION_RECEIPT_OUTCOMES, + PROMOTION_AUTHORIZATION_MODES, + PROMOTION_DESTINATION_SURFACES, + PROMOTION_REVERSAL_MODES, + PROMOTION_EVIDENCE_TYPES, + validatePromotionReceipt, + assertPromotionReceipt, +}; diff --git a/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js new file mode 100644 index 00000000..c8616af9 --- /dev/null +++ b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js @@ -0,0 +1,150 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const controlPlane = require('../src'); +const { validateCapabilityLedger } = require('../../../scripts/lib/capability-ledger'); +const { + validateCandidate, +} = require('../../jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract'); + +const ROOT = path.resolve(__dirname, '../../..'); +const FIXTURE_DIR = path.join(ROOT, 'tests/fixtures/foundation-contracts'); +const MANIFEST_PATH = path.join(FIXTURE_DIR, 'manifest.json'); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function validateIdentitySet(fixture) { + const errors = []; + if (fixture === null || typeof fixture !== 'object' || Array.isArray(fixture)) { + return ['identity set must be an object']; + } + for (const [kind, value] of Object.entries(fixture)) { + errors.push(...controlPlane.validateIdentity(value, kind).map((error) => `${kind}: ${error}`)); + } + for (const kind of controlPlane.IDENTITY_KINDS) { + if (!Object.hasOwn(fixture, kind)) errors.push(`identity set is missing ${kind}`); + } + return errors; +} + +const VALIDATORS = { + identity: (fixture) => controlPlane.validateIdentity(fixture), + 'identity-set': validateIdentitySet, + 'capability-ledger': validateCapabilityLedger, + candidate: validateCandidate, + 'promotion-receipt': controlPlane.validatePromotionReceipt, +}; + +test('foundation fixture manifest is closed, unique, and data-driven', () => { + const manifest = readJson(MANIFEST_PATH); + assert.equal(manifest.schemaVersion, 'jarvos.foundation-fixtures.v1'); + assert.deepEqual(Object.keys(manifest).sort(), ['cases', 'schemaVersion']); + assert.ok(Array.isArray(manifest.cases) && manifest.cases.length > 0); + const names = new Set(); + const files = new Set(); + for (const entry of manifest.cases) { + assert.deepEqual(Object.keys(entry).sort(), ['contract', 'file', 'name', 'valid']); + assert.equal(typeof entry.name, 'string'); + assert.ok(!names.has(entry.name), `duplicate fixture name: ${entry.name}`); + names.add(entry.name); + assert.ok(Object.hasOwn(VALIDATORS, entry.contract), `unknown contract: ${entry.contract}`); + assert.match(entry.file, /^[a-z0-9][a-z0-9.-]*\.json$/); + assert.ok(!files.has(entry.file), `duplicate fixture file: ${entry.file}`); + files.add(entry.file); + assert.equal(typeof entry.valid, 'boolean'); + assert.ok(fs.existsSync(path.join(FIXTURE_DIR, entry.file)), `missing fixture: ${entry.file}`); + } +}); + +test('foundation fixtures conform to their expected contract result', async (t) => { + const manifest = readJson(MANIFEST_PATH); + for (const entry of manifest.cases) { + await t.test(entry.name, () => { + const fixture = readJson(path.join(FIXTURE_DIR, entry.file)); + const errors = VALIDATORS[entry.contract](fixture); + assert.ok(Array.isArray(errors), `${entry.name} validator must return an error array`); + if (entry.valid) { + assert.deepEqual(errors, [], `${entry.name}: ${errors.join('; ')}`); + } else { + assert.ok(errors.length > 0, `${entry.name} should fail closed`); + } + }); + } +}); + +test('the tracked capability ledger conforms to the ledger contract', () => { + const ledger = readJson(path.join(ROOT, 'capability-ledger.json')); + assert.deepEqual(validateCapabilityLedger(ledger), []); +}); + +test('capability ledger rejects sparse record and evidence arrays', () => { + const ledger = readJson(path.join(ROOT, 'capability-ledger.json')); + assert.ok(validateCapabilityLedger({ ...ledger, records: new Array(1) }).length > 0); + + const sparseEvidence = structuredClone(ledger); + sparseEvidence.records[0].evidence = new Array(1); + assert.ok(validateCapabilityLedger(sparseEvidence).length > 0); +}); + +test('the tracked foundation ledger does not overstate an open draft pull request', () => { + const ledger = readJson(path.join(ROOT, 'capability-ledger.json')); + for (const record of ledger.records) { + assert.equal(record.specification, 'draft', `${record.capabilityId} must remain draft`); + assert.equal(record.repository, 'draft-pr', `${record.capabilityId} must remain draft-pr`); + assert.ok( + record.evidence.some((entry) => entry.type === 'pull-request' && entry.ref === 'pull/257'), + `${record.capabilityId} must identify its draft pull request evidence`, + ); + } +}); + +test('foundation architecture documents keep runtime ownership and proof tiers explicit', () => { + const productBoundary = fs.readFileSync( + path.join(ROOT, 'docs/architecture/product-category-and-boundaries.md'), + 'utf8', + ); + const ledgerBoundary = fs.readFileSync( + path.join(ROOT, 'docs/architecture/capability-truth-ledger.md'), + 'utf8', + ); + + const normalizedProductBoundary = productBoundary.replace(/\s+/g, ' '); + const normalizedLedgerBoundary = ledgerBoundary.replace(/\s+/g, ' '); + for (const claim of [ + 'jarvOS has no load-bearing harness', + 'Active Assistant is a jarvOS-owned service boundary', + 'Native session identifiers and their storage remain harness-owned', + 'Compatibility-read mode', + 'contract/fixture conformance', + 'does not claim adapter, installed, or live behavior', + ]) { + assert.ok(normalizedProductBoundary.includes(claim), `missing product boundary: ${claim}`); + } + assert.ok( + normalizedLedgerBoundary.includes('never canonical or repository-shipped'), + 'ledger documentation must distinguish draft PR work from shipped repository state', + ); +}); + +test('control-plane and ambient agree on candidate identity grammar', () => { + const valid = 'jarvos:candidate:example:c-identity-0001'; + const malformed = [ + 'jarvos:candidate:example:has/path', + 'jarvos:candidate:example:percent%20value', + 'JARVOS:candidate:example:uppercase', + 'jarvos:artifact:example:c-identity-0001', + ]; + assert.deepEqual(controlPlane.validateIdentity(valid, 'candidate'), []); + const base = readJson(path.join(FIXTURE_DIR, 'candidate-valid-memory.json')); + assert.deepEqual(validateCandidate({ ...base, candidateId: valid }), []); + for (const candidateId of malformed) { + assert.ok(controlPlane.validateIdentity(candidateId, 'candidate').length > 0, candidateId); + assert.ok(validateCandidate({ ...base, candidateId }).length > 0, candidateId); + } +}); diff --git a/modules/jarvos-control-plane/test/identity.test.js b/modules/jarvos-control-plane/test/identity.test.js new file mode 100644 index 00000000..67c42b72 --- /dev/null +++ b/modules/jarvos-control-plane/test/identity.test.js @@ -0,0 +1,82 @@ +'use strict'; + +const assert = require('node:assert'); +const test = require('node:test'); + +const { + IDENTITY_SCHEMA_VERSION, + IDENTITY_KINDS, + parseIdentity, + validateIdentity, + assertIdentity, +} = require('../src/index.js'); + +test('exposes a stable schema version and frozen kind list', () => { + assert.strictEqual(IDENTITY_SCHEMA_VERSION, 'jarvos.identity.v1'); + assert.ok(Object.isFrozen(IDENTITY_KINDS)); + assert.deepStrictEqual(IDENTITY_KINDS, [ + 'mind', 'installation', 'host', 'harness-instance', 'session', + 'source-event', 'candidate', 'artifact', 'project', 'policy', 'receipt', + ]); +}); + +test('every allowed kind validates, parses, and asserts', () => { + for (const kind of IDENTITY_KINDS) { + const value = `jarvos:${kind}:acme.tools:a0-9_.z`; + assert.deepStrictEqual(validateIdentity(value), []); + assert.deepStrictEqual(validateIdentity(value, kind), []); + assert.deepStrictEqual(parseIdentity(value), { + scheme: 'jarvos', kind, namespace: 'acme.tools', opaque: 'a0-9_.z', + }); + assert.strictEqual(assertIdentity(value, kind), value); + } +}); + +test('an expected kind must match exactly', () => { + const value = 'jarvos:project:acme:one'; + assert.deepStrictEqual(validateIdentity(value, 'project'), []); + assert.ok(validateIdentity(value, 'candidate').length > 0); + assert.strictEqual(parseIdentity(value, 'candidate'), null); + assert.throws(() => assertIdentity(value, 'candidate'), /identity/); +}); + +test('an unknown expected kind is rejected', () => { + assert.ok(validateIdentity('jarvos:project:acme:one', 'nonsense').length > 0); +}); + +test('malformed identities fail closed', () => { + const invalid = [ + 'JARVOS:project:acme:one', + 'jarvos:project:ACME:one', + 'jarvos:unknown:acme:one', + 'jarvos:project:acme', + 'jarvos:project:acme:one:two', + 'jarvos:project:acme:pa/th', + 'jarvos:project:acme:pa\\th', + 'jarvos:project:acme:pct%20', + 'jarvos:project:ac me:one', + 'jarvos:project::one', + 'jarvos:project:acme:', + `jarvos:project:acme:${'x'.repeat(300)}`, + 'jarvos:project:-acme:one', + '', + ]; + for (const value of invalid) { + assert.ok(validateIdentity(value).length > 0, `expected rejection: ${value}`); + assert.strictEqual(parseIdentity(value), null); + assert.throws(() => assertIdentity(value)); + } +}); + +test('non-string input is rejected without throwing during validation', () => { + for (const value of [null, undefined, 42, {}, []]) { + assert.ok(validateIdentity(value).length > 0); + assert.strictEqual(parseIdentity(value), null); + } +}); + +test('validation never mutates or normalizes the input value', () => { + const value = 'jarvos:session:acme:keep-as-is'; + assert.deepStrictEqual(validateIdentity(value), []); + assert.strictEqual(value, 'jarvos:session:acme:keep-as-is'); +}); diff --git a/modules/jarvos-control-plane/test/promotion-receipt.test.js b/modules/jarvos-control-plane/test/promotion-receipt.test.js new file mode 100644 index 00000000..dd66fa84 --- /dev/null +++ b/modules/jarvos-control-plane/test/promotion-receipt.test.js @@ -0,0 +1,164 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const contract = require('../src/promotion-receipt'); +const exported = require('../src'); + +const DIGEST = `sha256:${'a'.repeat(64)}`; + +function makeReceipt(overrides = {}) { + return { + schemaVersion: 'jarvos.promotion-receipt.v1', + receiptId: 'jarvos:receipt:foundation:r-0001', + operation: 'promotion', + outcome: 'committed', + candidateIds: ['jarvos:candidate:foundation:c-0001'], + policyId: 'jarvos:policy:foundation:promotion-v1', + authorization: { mode: 'user-reviewed' }, + destination: { + surface: 'memory', + artifactId: 'jarvos:artifact:foundation:a-0001', + revisionBefore: null, + revisionAfter: 'rev-0001', + reversalMode: 'supersession', + }, + recordedAt: '2026-08-31T12:00:00Z', + evidence: [{ type: 'verification', ref: 'verification:memory:0001', digest: DIGEST }], + ...overrides, + }; +} + +test('exports a stable schema and frozen enums through the package', () => { + assert.equal(contract.PROMOTION_RECEIPT_SCHEMA_VERSION, 'jarvos.promotion-receipt.v1'); + for (const value of [ + contract.PROMOTION_RECEIPT_OPERATIONS, + contract.PROMOTION_RECEIPT_OUTCOMES, + contract.PROMOTION_AUTHORIZATION_MODES, + contract.PROMOTION_DESTINATION_SURFACES, + contract.PROMOTION_REVERSAL_MODES, + contract.PROMOTION_EVIDENCE_TYPES, + ]) assert.ok(Object.isFrozen(value)); + assert.equal(exported.validatePromotionReceipt, contract.validatePromotionReceipt); +}); + +test('accepts committed promotions on every destination surface', () => { + for (const surface of contract.PROMOTION_DESTINATION_SURFACES) { + const receipt = makeReceipt({ destination: { ...makeReceipt().destination, surface } }); + assert.deepEqual(contract.validatePromotionReceipt(receipt), [], surface); + assert.equal(contract.assertPromotionReceipt(receipt), receipt); + } +}); + +test('committed destination state may explicitly have no future reversal mode', () => { + const receipt = makeReceipt({ + destination: { ...makeReceipt().destination, reversalMode: 'none' }, + }); + assert.deepEqual(contract.validatePromotionReceipt(receipt), []); +}); + +test('non-committed outcomes report no mutation or reversal claim', () => { + for (const outcome of ['already_satisfied', 'deferred', 'conflict', 'failed']) { + const receipt = makeReceipt({ + outcome, + destination: { ...makeReceipt().destination, revisionAfter: null, reversalMode: 'none' }, + }); + assert.deepEqual(contract.validatePromotionReceipt(receipt), [], outcome); + assert.ok(contract.validatePromotionReceipt({ + ...receipt, + destination: { ...receipt.destination, revisionAfter: 'invented' }, + }).length > 0); + assert.ok(contract.validatePromotionReceipt({ + ...receipt, + destination: { ...receipt.destination, reversalMode: 'rollback' }, + }).length > 0); + } +}); + +test('promotion requires candidates and forbids a predecessor', () => { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ candidateIds: [] })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ candidateIds: new Array(1) })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + predecessorReceiptId: 'jarvos:receipt:foundation:r-0000', + })).length > 0); +}); + +test('non-promotion operations require a receipt predecessor', () => { + for (const operation of ['supersession', 'retraction', 'rollback', 'correction']) { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ operation })).length > 0, operation); + const receipt = makeReceipt({ + operation, + candidateIds: [], + predecessorReceiptId: 'jarvos:receipt:foundation:r-0000', + }); + assert.deepEqual(contract.validatePromotionReceipt(receipt), [], operation); + } +}); + +test('identity kinds are enforced for all identity-bearing fields', () => { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ receiptId: 'jarvos:candidate:foundation:r-0001' })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ candidateIds: ['jarvos:artifact:foundation:c-0001'] })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ policyId: 'jarvos:receipt:foundation:p-0001' })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + destination: { ...makeReceipt().destination, artifactId: 'jarvos:project:foundation:a-0001' }, + })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + operation: 'rollback', predecessorReceiptId: 'jarvos:policy:foundation:r-0000', + })).length > 0); +}); + +test('unknown fields and enums fail closed at every nesting level', () => { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ rawContent: 'no' })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ authorization: { mode: 'user-reviewed', actor: 'x' } })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + destination: { ...makeReceipt().destination, path: '/private/note.md' }, + })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + evidence: [{ ...makeReceipt().evidence[0], raw: 'content' }], + })).length > 0); + for (const overrides of [ + { operation: 'write' }, + { outcome: 'success' }, + { authorization: { mode: 'implicit' } }, + { destination: { ...makeReceipt().destination, surface: 'transcripts' } }, + { destination: { ...makeReceipt().destination, reversalMode: 'delete' } }, + { evidence: [{ type: 'raw-content', ref: 'evidence:one', digest: DIGEST }] }, + ]) assert.ok(contract.validatePromotionReceipt(makeReceipt(overrides)).length > 0); +}); + +test('evidence is non-empty, bounded, path-free with sha256 digests', () => { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ evidence: [] })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ evidence: new Array(1) })).length > 0); + for (const ref of ['/tmp/evidence', '../evidence', 'file://evidence', 'file:evidence', 'evidence one', 'Evidence:one']) { + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + evidence: [{ type: 'verification', ref, digest: DIGEST }], + })).length > 0, ref); + } + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + evidence: [{ type: 'verification', ref: 'evidence:one', digest: 'md5:short' }], + })).length > 0); + assert.ok(contract.validatePromotionReceipt(makeReceipt({ + destination: { ...makeReceipt().destination, revisionAfter: '/tmp/revision' }, + })).length > 0); +}); + +test('timestamps must be real ISO UTC instants', () => { + for (const recordedAt of [ + '2026-08-31', + '2026-08-31T12:00:00+01:00', + '2026-13-01T00:00:00Z', + '2026-02-30T00:00:00Z', + '2026-08-31T24:00:00Z', + ]) assert.ok(contract.validatePromotionReceipt(makeReceipt({ recordedAt })).length > 0, recordedAt); + assert.deepEqual(contract.validatePromotionReceipt(makeReceipt({ + recordedAt: '0000-02-29T00:00:00Z', + })), []); +}); + +test('non-object input fails closed and assertion throws', () => { + for (const value of [null, undefined, 'receipt', 42, []]) { + assert.ok(contract.validatePromotionReceipt(value).length > 0); + } + assert.throws(() => contract.assertPromotionReceipt(makeReceipt({ outcome: 'success' })), /promotion receipt/); +}); diff --git a/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md b/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md new file mode 100644 index 00000000..140e3892 --- /dev/null +++ b/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md @@ -0,0 +1,72 @@ +# Candidate Envelope + +The candidate envelope is a portable `@jarvos/ambient` contract for a +non-authoritative proposal bound to eligible source evidence. It is a value, +not a record. It carries no mutable status and cannot itself represent +promotion, rejection, completion, or recall. A candidate never satisfies +recall, completion, a Project identity, or authoritative memory. + +Schema version: `jarvos.candidate.v1`. + +The validator reads no host, process, environment, network, store, or Vault +state. The module exports no recall, completion, write, store, or promotion +function. Ineligible material—secret privacy, untrusted recall, tool output, +unknown ownership, or unknown trust—is rejected before a candidate is +constructed and is therefore not representable in this envelope. + +## Shape + +`assertCandidate()` returns an immutable deep-cloned candidate object with +exactly these fields. `validateCandidate()` only validates an input and never +mutates or freezes it. Any unknown top-level or nested field fails closed. + +| Field | Requirement | +| --- | --- | +| `schemaVersion` | `jarvos.candidate.v1` | +| `candidateId` | identity of kind `candidate` | +| `candidateType` | `note-draft`, `journal-suggestion`, `memory-unit`, `ontology-inquiry`, `project-signal`, `skill-proposal`, or `work-proposal` | +| `authority` | required literal `non-authoritative` | +| `sources` | non-empty, bounded array of `{ sourceEventId, evidenceDigest }` | +| `privacyTier` | `public`, `local-private`, `private`, or `sensitive` | +| `sourceTrust` | `user-authored` or `assistant-derived` | +| `construction` | `{ extractorId, extractorVersion, eligibilityPolicyId }` | +| `dedupeKey` | bounded opaque string | +| `createdAt` | ISO instant | +| `expiresAt` | ISO instant later than `createdAt` | +| `proposal` | bounded `{ title, summary }` | + +Each `sources` entry contains only `sourceEventId` (identity of kind +`source-event`) and `evidenceDigest` (`sha256:<64 hex>`). Source-event IDs +cannot repeat within a candidate. Candidates carry evidence digests and source +identity, never raw evidence. + +`construction.eligibilityPolicyId` is an identity of kind `policy` naming the +policy under which the extractor judged the material eligible. `proposal` is +limited to a bounded `title` and `summary` and contains no raw transcript, note +content, recall text, completion output, or destination. + +## Invariants + +- An asserted envelope is immutable and has no mutable `status`. +- `authority` is the literal `non-authoritative`; there is no authoritative + member. +- `secret` privacy and any trust other than `user-authored` or + `assistant-derived` are rejected. +- Fields such as `authoritative`, `verified`, `completed`, `status`, + `destination`, `recallText`, `text`, `content`, and `transcript` are unknown + and rejected at every level. +- A `project-signal` may reference source evidence but cannot mint or replace a + Project identity; no Project identity is a candidate field. + +## Identity grammar + +Ambient has no control-plane dependency, so it checks an already-issued +identifier with a small private shape test. The grammar is +`jarvos:::`, all lowercase and at most 256 characters, +with `namespace` and `opaque` each matching +`[a-z0-9][a-z0-9._-]{0,63}`. It never resolves, dereferences, normalizes, or +infers an identifier. Cross-contract conformance fixtures keep this grammar +aligned with `@jarvos/control-plane`. + +The contract is reachable from `@jarvos/ambient`, `@jarvos/ambient/intent`, and +`@jarvos/ambient/intent/candidate-contract`. diff --git a/modules/jarvos-secondbrain/packages/jarvos-ambient/package.json b/modules/jarvos-secondbrain/packages/jarvos-ambient/package.json index 545024a6..46c430ea 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/package.json +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/package.json @@ -15,7 +15,8 @@ "./intent/salience-detector": "./src/intent/salience-detector.js", "./intent/keyword-capture-router": "./src/intent/keyword-capture-router.js", "./intent/retroactive-capture": "./src/intent/retroactive-capture.js", - "./intent/capture-contract": "./src/intent/capture-contract.js" + "./intent/capture-contract": "./src/intent/capture-contract.js", + "./intent/candidate-contract": "./src/intent/candidate-contract.js" }, "files": [ "src", diff --git a/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js new file mode 100644 index 00000000..27112353 --- /dev/null +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js @@ -0,0 +1,246 @@ +'use strict'; + +// Non-authoritative candidate envelope for the ambient intent layer. A +// validated value is a bounded proposal bound to eligible source evidence. An +// asserted value is returned as an immutable clone. Neither is a record: it +// carries no mutable status and cannot itself represent promotion, rejection, +// completion, or recall. It never carries raw transcript, note content, recall +// text, completion output, or a destination. + +const CANDIDATE_SCHEMA_VERSION = 'jarvos.candidate.v1'; + +const CANDIDATE_TYPES = Object.freeze([ + 'note-draft', + 'journal-suggestion', + 'memory-unit', + 'ontology-inquiry', + 'project-signal', + 'skill-proposal', + 'work-proposal', +]); + +const CANDIDATE_AUTHORITY = 'non-authoritative'; +const CANDIDATE_PRIVACY_TIERS = Object.freeze([ + 'public', + 'local-private', + 'private', + 'sensitive', +]); +const CANDIDATE_SOURCE_TRUST = Object.freeze([ + 'user-authored', + 'assistant-derived', +]); + +const CANDIDATE_KEYS = [ + 'schemaVersion', 'candidateId', 'candidateType', 'authority', 'sources', + 'privacyTier', 'sourceTrust', 'construction', 'dedupeKey', 'createdAt', + 'expiresAt', 'proposal', +]; +const CONSTRUCTION_KEYS = ['extractorId', 'extractorVersion', 'eligibilityPolicyId']; +const SOURCE_KEYS = ['sourceEventId', 'evidenceDigest']; +const PROPOSAL_KEYS = ['title', 'summary']; + +const MAX_IDENTITY_LENGTH = 256; +const MAX_SOURCES = 64; +const MAX_DEDUPE_KEY_LENGTH = 256; +const MAX_CONSTRUCTION_FIELD_LENGTH = 128; +const MAX_TITLE_LENGTH = 200; +const MAX_SUMMARY_LENGTH = 2000; + +const IDENTITY_SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/; +const EVIDENCE_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/; +const ISO_INSTANT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|([+-])(\d{2}):(\d{2}))$/; + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function hasExactKeys(value, keys) { + return isPlainObject(value) + && Object.keys(value).every((key) => keys.includes(key)) + && keys.every((key) => Object.hasOwn(value, key)); +} + +function isBoundedString(value, max) { + return typeof value === 'string' && value.length > 0 && value.length <= max; +} + +function isIdentityOfKind(value, kind) { + if (typeof value !== 'string' || value.length === 0 || value.length > MAX_IDENTITY_LENGTH) return false; + if (value !== value.toLowerCase()) return false; + const segments = value.split(':'); + if (segments.length !== 4) return false; + const [scheme, actualKind, namespace, opaque] = segments; + if (scheme !== 'jarvos' || actualKind !== kind) return false; + return IDENTITY_SEGMENT_PATTERN.test(namespace) && IDENTITY_SEGMENT_PATTERN.test(opaque); +} + +function daysInMonth(year, month) { + if (month === 2) { + const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + return leap ? 29 : 28; + } + return [4, 6, 9, 11].includes(month) ? 30 : 31; +} + +function isoInstantMs(value) { + if (typeof value !== 'string') return null; + const match = ISO_INSTANT_PATTERN.exec(value); + if (!match) return null; + const [, yearText, monthText, dayText, hourText, minuteText, secondText, + offsetSign, offsetHourText, offsetMinuteText] = match; + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + const hour = Number(hourText); + const minute = Number(minuteText); + const second = Number(secondText); + if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59) return null; + if (day > daysInMonth(year, month)) return null; + if (offsetSign) { + const offsetHour = Number(offsetHourText); + const offsetMinute = Number(offsetMinuteText); + if (offsetHour > 14 || offsetMinute > 59 || (offsetHour === 14 && offsetMinute !== 0)) return null; + } + const ms = Date.parse(value); + return Number.isNaN(ms) ? null : ms; +} + +function validateSourceEntry(entry, index, seen, errors) { + const label = `sources[${index}]`; + if (!hasExactKeys(entry, SOURCE_KEYS)) { + errors.push(`${label} must contain only sourceEventId and evidenceDigest`); + return; + } + if (!isIdentityOfKind(entry.sourceEventId, 'source-event')) { + errors.push(`${label} sourceEventId must be a source-event identity`); + } else if (seen.has(entry.sourceEventId)) { + errors.push(`${label} duplicates an earlier sourceEventId`); + } else { + seen.add(entry.sourceEventId); + } + if (typeof entry.evidenceDigest !== 'string' || !EVIDENCE_DIGEST_PATTERN.test(entry.evidenceDigest)) { + errors.push(`${label} evidenceDigest must be sha256:<64 hex>`); + } +} + +function validateConstruction(construction, errors) { + if (!hasExactKeys(construction, CONSTRUCTION_KEYS)) { + errors.push('construction must contain only extractorId, extractorVersion, and eligibilityPolicyId'); + return; + } + if (!isBoundedString(construction.extractorId, MAX_CONSTRUCTION_FIELD_LENGTH)) { + errors.push('construction.extractorId must be a bounded non-empty string'); + } + if (!isBoundedString(construction.extractorVersion, MAX_CONSTRUCTION_FIELD_LENGTH)) { + errors.push('construction.extractorVersion must be a bounded non-empty string'); + } + if (!isIdentityOfKind(construction.eligibilityPolicyId, 'policy')) { + errors.push('construction.eligibilityPolicyId must be a policy identity'); + } +} + +function validateProposal(proposal, errors) { + if (!hasExactKeys(proposal, PROPOSAL_KEYS)) { + errors.push('proposal must contain only title and summary'); + return; + } + if (!isBoundedString(proposal.title, MAX_TITLE_LENGTH)) { + errors.push('proposal.title must be a bounded non-empty string'); + } + if (!isBoundedString(proposal.summary, MAX_SUMMARY_LENGTH)) { + errors.push('proposal.summary must be a bounded non-empty string'); + } +} + +function deepFreeze(value) { + if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value; + Object.freeze(value); + for (const child of Object.values(value)) deepFreeze(child); + return value; +} + +function validateCandidate(candidate) { + const errors = []; + if (!isPlainObject(candidate)) { + errors.push('candidate must be an object'); + return errors; + } + if (!Object.keys(candidate).every((key) => CANDIDATE_KEYS.includes(key))) { + errors.push('candidate contains an unknown field'); + } + for (const key of CANDIDATE_KEYS) { + if (!Object.hasOwn(candidate, key)) errors.push(`candidate is missing ${key}`); + } + + if (candidate.schemaVersion !== CANDIDATE_SCHEMA_VERSION) { + errors.push('candidate schemaVersion is unsupported'); + } + if (!isIdentityOfKind(candidate.candidateId, 'candidate')) { + errors.push('candidate candidateId must be a candidate identity'); + } + if (!CANDIDATE_TYPES.includes(candidate.candidateType)) { + errors.push('candidate has an unknown candidateType'); + } + if (candidate.authority !== CANDIDATE_AUTHORITY) { + errors.push('candidate authority must be the literal "non-authoritative"'); + } + if (!CANDIDATE_PRIVACY_TIERS.includes(candidate.privacyTier)) { + errors.push('candidate has an unknown or ineligible privacyTier'); + } + if (!CANDIDATE_SOURCE_TRUST.includes(candidate.sourceTrust)) { + errors.push('candidate has an unknown or ineligible sourceTrust'); + } + if (!isBoundedString(candidate.dedupeKey, MAX_DEDUPE_KEY_LENGTH)) { + errors.push('candidate dedupeKey must be a bounded non-empty string'); + } + + if (!Array.isArray(candidate.sources) || candidate.sources.length === 0 || candidate.sources.length > MAX_SOURCES) { + errors.push('candidate sources must be a bounded non-empty array'); + } else { + const seen = new Set(); + for (let index = 0; index < candidate.sources.length; index += 1) { + validateSourceEntry(candidate.sources[index], index, seen, errors); + } + } + + if (!isPlainObject(candidate.construction)) { + errors.push('candidate construction must be an object'); + } else { + validateConstruction(candidate.construction, errors); + } + + if (!isPlainObject(candidate.proposal)) { + errors.push('candidate proposal must be an object'); + } else { + validateProposal(candidate.proposal, errors); + } + + const createdMs = isoInstantMs(candidate.createdAt); + const expiresMs = isoInstantMs(candidate.expiresAt); + if (createdMs === null) errors.push('candidate createdAt must be a real ISO instant'); + if (expiresMs === null) errors.push('candidate expiresAt must be a real ISO instant'); + if (createdMs !== null && expiresMs !== null && expiresMs <= createdMs) { + errors.push('candidate expiresAt must be later than createdAt'); + } + + return errors; +} + +function assertCandidate(candidate) { + const errors = validateCandidate(candidate); + if (errors.length > 0) { + throw new Error(`invalid jarvos candidate: ${errors.join('; ')}`); + } + return deepFreeze(structuredClone(candidate)); +} + +module.exports = { + CANDIDATE_SCHEMA_VERSION, + CANDIDATE_TYPES, + CANDIDATE_AUTHORITY, + CANDIDATE_PRIVACY_TIERS, + CANDIDATE_SOURCE_TRUST, + validateCandidate, + assertCandidate, +}; diff --git a/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/index.js b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/index.js index 6d3a15ee..7c4b8d8c 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/index.js +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/index.js @@ -1,6 +1,7 @@ 'use strict'; module.exports = { + ...require('./candidate-contract'), ...require('./capture-contract'), ...require('./keyword-capture-router'), ...require('./retroactive-capture'), diff --git a/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js b/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js new file mode 100644 index 00000000..e32ed72f --- /dev/null +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js @@ -0,0 +1,171 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const contract = require('../src/intent/candidate-contract'); +const intent = require('../src/intent'); +const ambient = require('../src'); + +const DIGEST_A = `sha256:${'a'.repeat(64)}`; +const DIGEST_B = `sha256:${'b'.repeat(64)}`; + +function makeCandidate(overrides = {}) { + return { + schemaVersion: 'jarvos.candidate.v1', + candidateId: 'jarvos:candidate:ambient:c-0001', + candidateType: 'memory-unit', + authority: 'non-authoritative', + sources: [ + { sourceEventId: 'jarvos:source-event:ambient:e-0001', evidenceDigest: DIGEST_A }, + ], + privacyTier: 'local-private', + sourceTrust: 'user-authored', + construction: { + extractorId: 'salience-detector', + extractorVersion: '2.0.0', + eligibilityPolicyId: 'jarvos:policy:ambient:eligibility-v1', + }, + dedupeKey: 'memory-unit:local-private:0001', + createdAt: '2026-08-31T12:00:00Z', + expiresAt: '2026-09-30T12:00:00Z', + proposal: { + title: 'Prefer pure contracts before consumers', + summary: 'The author decided to land validators and fixtures before any store or writer.', + }, + ...overrides, + }; +} + +test('exposes a stable schema version, literal authority, and frozen enums', () => { + assert.equal(contract.CANDIDATE_SCHEMA_VERSION, 'jarvos.candidate.v1'); + assert.equal(contract.CANDIDATE_AUTHORITY, 'non-authoritative'); + assert.ok(Object.isFrozen(contract.CANDIDATE_TYPES)); + assert.ok(Object.isFrozen(contract.CANDIDATE_PRIVACY_TIERS)); + assert.ok(Object.isFrozen(contract.CANDIDATE_SOURCE_TRUST)); +}); + +test('the contract is reachable through the intent namespace and package subpath', () => { + assert.equal(intent.validateCandidate, contract.validateCandidate); + assert.equal(intent.assertCandidate, contract.assertCandidate); + assert.equal(ambient.validateCandidate, contract.validateCandidate); +}); + +test('the module exposes no recall, completion, write, store, or promotion function', () => { + assert.deepEqual(Object.keys(contract).sort(), [ + 'CANDIDATE_AUTHORITY', + 'CANDIDATE_PRIVACY_TIERS', + 'CANDIDATE_SCHEMA_VERSION', + 'CANDIDATE_SOURCE_TRUST', + 'CANDIDATE_TYPES', + 'assertCandidate', + 'validateCandidate', + ]); +}); + +test('valid candidate types assert as immutable copies without mutation powers', () => { + for (const candidateType of contract.CANDIDATE_TYPES) { + const candidate = makeCandidate({ candidateType }); + assert.deepEqual(contract.validateCandidate(candidate), [], candidateType); + const asserted = contract.assertCandidate(candidate); + assert.notEqual(asserted, candidate); + assert.deepEqual(asserted, candidate); + assert.ok(Object.isFrozen(asserted)); + assert.ok(Object.isFrozen(asserted.sources)); + assert.ok(Object.isFrozen(asserted.sources[0])); + assert.ok(Object.isFrozen(asserted.proposal)); + candidate.proposal.title = 'mutable input'; + assert.notEqual(asserted.proposal.title, candidate.proposal.title); + assert.throws(() => { asserted.proposal.title = 'cannot mutate'; }, TypeError); + } +}); + +test('project signals cannot attach or mint a Project identity', () => { + assert.ok(contract.validateCandidate(makeCandidate({ + candidateType: 'project-signal', + projectId: 'jarvos:project:ambient:p-0001', + })).length > 0); +}); + +test('unknown version, type, authority, and mutable or authoritative fields fail closed', () => { + assert.ok(contract.validateCandidate(makeCandidate({ schemaVersion: 'jarvos.candidate.v2' })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ candidateType: 'authoritative-memory' })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ authority: 'authoritative' })).length > 0); + for (const extra of ['authoritative', 'verified', 'completed', 'status', 'destination', 'recallText', 'text', 'content', 'transcript']) { + assert.ok(contract.validateCandidate(makeCandidate({ [extra]: 'x' })).length > 0, extra); + } +}); + +test('secret privacy and ineligible trust values are not representable', () => { + assert.ok(contract.validateCandidate(makeCandidate({ privacyTier: 'secret' })).length > 0); + for (const sourceTrust of ['untrusted', 'tool', 'unknown', 'tool-output']) { + assert.ok(contract.validateCandidate(makeCandidate({ sourceTrust })).length > 0, sourceTrust); + } + assert.deepEqual(contract.validateCandidate(makeCandidate({ sourceTrust: 'assistant-derived' })), []); +}); + +test('sources must be unique evidence pointers with source-event identities and sha256 digests', () => { + assert.ok(contract.validateCandidate(makeCandidate({ sources: [] })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + sources: [ + { sourceEventId: 'jarvos:source-event:ambient:e-0001', evidenceDigest: DIGEST_A }, + { sourceEventId: 'jarvos:source-event:ambient:e-0001', evidenceDigest: DIGEST_B }, + ], + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + sources: [{ sourceEventId: 'jarvos:candidate:ambient:e-0001', evidenceDigest: DIGEST_A }], + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + sources: [{ sourceEventId: 'jarvos:source-event:ambient:e-0001', evidenceDigest: 'md5:short' }], + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + sources: [{ sourceEventId: 'jarvos:source-event:ambient:e-0001', evidenceDigest: DIGEST_A, recallText: 'x' }], + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ sources: new Array(1) })).length > 0); +}); + +test('construction rejects the wrong identity kind and unknown nested fields', () => { + assert.ok(contract.validateCandidate(makeCandidate({ + construction: { extractorId: 'x', extractorVersion: '1.0.0', eligibilityPolicyId: 'jarvos:candidate:ambient:not-a-policy' }, + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + construction: { + extractorId: 'x', extractorVersion: '1.0.0', + eligibilityPolicyId: 'jarvos:policy:ambient:eligibility-v1', recallText: 'x', + }, + })).length > 0); +}); + +test('proposal is bounded to title and summary', () => { + assert.ok(contract.validateCandidate(makeCandidate({ + proposal: { title: 'ok', summary: 'ok', text: 'raw' }, + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ proposal: { title: 'only title' } })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ proposal: { title: '', summary: 'x' } })).length > 0); +}); + +test('timestamps must be real ISO instants with expiry after creation', () => { + assert.ok(contract.validateCandidate(makeCandidate({ createdAt: '2026-08-31' })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ createdAt: '2026-13-31T00:00:00Z' })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ createdAt: '2026-02-30T00:00:00Z' })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ expiresAt: undefined })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + createdAt: '2026-08-31T12:00:00Z', expiresAt: '2026-08-31T12:00:00Z', + })).length > 0); + assert.ok(contract.validateCandidate(makeCandidate({ + createdAt: '2026-09-30T12:00:00Z', expiresAt: '2026-08-31T12:00:00Z', + })).length > 0); + assert.deepEqual(contract.validateCandidate(makeCandidate({ + createdAt: '0000-02-29T00:00:00Z', expiresAt: '0000-03-01T00:00:00Z', + })), []); + for (const createdAt of ['2026-08-31T12:00:00+14:01', '2026-08-31T12:00:00-14:01', '2026-08-31T12:00:00+23:59']) { + assert.ok(contract.validateCandidate(makeCandidate({ createdAt })).length > 0, createdAt); + } +}); + +test('non-object input fails closed and assertion throws on invalid input', () => { + for (const value of [null, undefined, 'candidate', 42, []]) { + assert.ok(contract.validateCandidate(value).length > 0); + } + assert.throws(() => contract.assertCandidate(makeCandidate({ authority: 'authoritative' })), /candidate/); +}); diff --git a/scripts/lib/capability-ledger.js b/scripts/lib/capability-ledger.js new file mode 100644 index 00000000..55626542 --- /dev/null +++ b/scripts/lib/capability-ledger.js @@ -0,0 +1,158 @@ +'use strict'; + +// Capability truth ledger contract. Records are assertions with evidence +// pointers, not discovery, activation proof, or live probes. The validator +// reads no repository, host, process, or network state. It checks only the +// shape and bounds of an in-memory ledger and never infers one truth dimension +// from another. + +const CAPABILITY_LEDGER_SCHEMA_VERSION = 'jarvos.capability-ledger.v1'; + +const SPECIFICATION_STATES = Object.freeze(['absent', 'draft', 'canonical']); +const IMPLEMENTATION_STATES = Object.freeze(['absent', 'partial', 'complete']); +const REPOSITORY_STATES = Object.freeze(['local-only', 'draft-pr', 'merged', 'released']); +const VERIFICATION_STATES = Object.freeze(['untested', 'fixture-proven', 'clean-install-proven', 'live-canary-proven']); +const ACTIVATION_STATES = Object.freeze(['inactive', 'test-fixture', 'disposable', 'enrolled-host', 'production', 'unknown']); +const AUTHORITY_STATES = Object.freeze(['none', 'read-only', 'proposed', 'active', 'conflicted']); +const EVIDENCE_TYPES = Object.freeze(['repo-path', 'pull-request', 'test', 'document', 'commit']); + +const RECORD_KEYS = [ + 'capabilityId', 'title', 'specification', 'implementation', 'repository', + 'verification', 'activation', 'authority', 'evidence', 'assertedOn', 'notes', +]; +const REQUIRED_RECORD_KEYS = RECORD_KEYS.filter((key) => key !== 'notes'); +const MAX_RECORDS = 256; +const MAX_EVIDENCE = 32; +const MAX_TITLE_LENGTH = 200; +const MAX_NOTES_LENGTH = 1000; +const MAX_REF_LENGTH = 256; +const CAPABILITY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/; + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function hasOnlyKeys(value, keys) { + return isPlainObject(value) && Object.keys(value).every((key) => keys.includes(key)); +} + +function isBoundedString(value, max) { + return typeof value === 'string' && value.length > 0 && value.length <= max; +} + +function isDate(value) { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; + const parsed = new Date(`${value}T00:00:00Z`); + return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value; +} + +function isValidEvidenceRef(ref) { + if (typeof ref !== 'string' || ref.length === 0 || ref.length > MAX_REF_LENGTH) return false; + if (/\s/.test(ref)) return false; + if (ref.startsWith('/') || ref.startsWith('\\')) return false; + if (/^[a-z]:[\\/]/i.test(ref)) return false; + if (/^[a-z][a-z0-9+.-]*:/i.test(ref)) return false; + return !ref.split(/[\\/]/).includes('..'); +} + +function validateEvidenceEntry(entry, index, recordLabel, errors) { + const label = `${recordLabel}.evidence[${index}]`; + if (!hasOnlyKeys(entry, ['type', 'ref']) + || !Object.hasOwn(entry, 'type') || !Object.hasOwn(entry, 'ref')) { + errors.push(`${label} must contain only type and ref`); + return; + } + if (!EVIDENCE_TYPES.includes(entry.type)) { + errors.push(`${label} has an unknown evidence type`); + } + if (!isValidEvidenceRef(entry.ref)) { + errors.push(`${label} ref must be a bounded, relative, scheme-free reference`); + } +} + +function validateRecord(record, index, seenIds, errors) { + const label = `records[${index}]`; + if (!isPlainObject(record)) { + errors.push(`${label} must be an object`); + return; + } + if (!hasOnlyKeys(record, RECORD_KEYS)) { + errors.push(`${label} contains an unknown field`); + } + for (const key of REQUIRED_RECORD_KEYS) { + if (!Object.hasOwn(record, key)) errors.push(`${label} is missing ${key}`); + } + if (typeof record.capabilityId !== 'string' || !CAPABILITY_ID_PATTERN.test(record.capabilityId)) { + errors.push(`${label} capabilityId must be a bounded lowercase slug`); + } else if (seenIds.has(record.capabilityId)) { + errors.push(`${label} duplicates an earlier capabilityId`); + } else { + seenIds.add(record.capabilityId); + } + if (!isBoundedString(record.title, MAX_TITLE_LENGTH)) { + errors.push(`${label} title must be a bounded non-empty string`); + } + if (!SPECIFICATION_STATES.includes(record.specification)) errors.push(`${label} has an unknown specification state`); + if (!IMPLEMENTATION_STATES.includes(record.implementation)) errors.push(`${label} has an unknown implementation state`); + if (!REPOSITORY_STATES.includes(record.repository)) errors.push(`${label} has an unknown repository state`); + if (!VERIFICATION_STATES.includes(record.verification)) errors.push(`${label} has an unknown verification state`); + if (!ACTIVATION_STATES.includes(record.activation)) errors.push(`${label} has an unknown activation state`); + if (!AUTHORITY_STATES.includes(record.authority)) errors.push(`${label} has an unknown authority state`); + if (!Array.isArray(record.evidence) || record.evidence.length === 0 || record.evidence.length > MAX_EVIDENCE) { + errors.push(`${label} evidence must be a bounded non-empty array`); + } else { + for (let evidenceIndex = 0; evidenceIndex < record.evidence.length; evidenceIndex += 1) { + validateEvidenceEntry(record.evidence[evidenceIndex], evidenceIndex, label, errors); + } + } + if (!isDate(record.assertedOn)) { + errors.push(`${label} assertedOn must be an ISO calendar date`); + } + if (Object.hasOwn(record, 'notes') && !isBoundedString(record.notes, MAX_NOTES_LENGTH)) { + errors.push(`${label} notes must be a bounded non-empty string when present`); + } +} + +function validateCapabilityLedger(ledger) { + const errors = []; + if (!isPlainObject(ledger)) { + errors.push('ledger must be an object'); + return errors; + } + if (!hasOnlyKeys(ledger, ['schemaVersion', 'records'])) { + errors.push('ledger contains an unknown top-level field'); + } + if (ledger.schemaVersion !== CAPABILITY_LEDGER_SCHEMA_VERSION) { + errors.push('ledger schemaVersion is unsupported'); + } + if (!Array.isArray(ledger.records) || ledger.records.length === 0 || ledger.records.length > MAX_RECORDS) { + errors.push('ledger records must be a bounded non-empty array'); + return errors; + } + const seenIds = new Set(); + for (let index = 0; index < ledger.records.length; index += 1) { + validateRecord(ledger.records[index], index, seenIds, errors); + } + return errors; +} + +function assertCapabilityLedger(ledger) { + const errors = validateCapabilityLedger(ledger); + if (errors.length > 0) { + throw new Error(`invalid capability ledger: ${errors.join('; ')}`); + } + return ledger; +} + +module.exports = { + CAPABILITY_LEDGER_SCHEMA_VERSION, + SPECIFICATION_STATES, + IMPLEMENTATION_STATES, + REPOSITORY_STATES, + VERIFICATION_STATES, + ACTIVATION_STATES, + AUTHORITY_STATES, + EVIDENCE_TYPES, + validateCapabilityLedger, + assertCapabilityLedger, +}; diff --git a/tests/fixtures/foundation-contracts/candidate-invalid-authoritative.json b/tests/fixtures/foundation-contracts/candidate-invalid-authoritative.json new file mode 100644 index 00000000..8d91f4e8 --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-invalid-authoritative.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0101", + "candidateType": "memory-unit", + "authority": "authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0101", "evidenceDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "privacyTier": "private", + "sourceTrust": "user-authored", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "invalid:authoritative", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-30T12:00:00Z", + "proposal": { "title": "Invalid authority claim", "summary": "A candidate may never claim authority." } +} diff --git a/tests/fixtures/foundation-contracts/candidate-invalid-bad-digest.json b/tests/fixtures/foundation-contracts/candidate-invalid-bad-digest.json new file mode 100644 index 00000000..72fa0349 --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-invalid-bad-digest.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0104", + "candidateType": "memory-unit", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0104", "evidenceDigest": "md5:not-source-evidence" }], + "privacyTier": "private", + "sourceTrust": "user-authored", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "invalid:bad-digest", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-30T12:00:00Z", + "proposal": { "title": "Bad digest", "summary": "Source evidence must use the canonical digest format." } +} diff --git a/tests/fixtures/foundation-contracts/candidate-invalid-missing-expiry.json b/tests/fixtures/foundation-contracts/candidate-invalid-missing-expiry.json new file mode 100644 index 00000000..c7d1372e --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-invalid-missing-expiry.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0103", + "candidateType": "journal-suggestion", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0103", "evidenceDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "privacyTier": "private", + "sourceTrust": "user-authored", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "invalid:missing-expiry", + "createdAt": "2026-08-31T12:00:00Z", + "proposal": { "title": "Missing expiry", "summary": "Candidate lifetimes are bounded." } +} diff --git a/tests/fixtures/foundation-contracts/candidate-invalid-status.json b/tests/fixtures/foundation-contracts/candidate-invalid-status.json new file mode 100644 index 00000000..80530a06 --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-invalid-status.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0105", + "candidateType": "memory-unit", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0105", "evidenceDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "privacyTier": "private", + "sourceTrust": "user-authored", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "invalid:mutable-status", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-30T12:00:00Z", + "proposal": { "title": "Mutable status", "summary": "Candidate values do not carry lifecycle state." }, + "status": "approved" +} diff --git a/tests/fixtures/foundation-contracts/candidate-invalid-untrusted.json b/tests/fixtures/foundation-contracts/candidate-invalid-untrusted.json new file mode 100644 index 00000000..6de308e5 --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-invalid-untrusted.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0102", + "candidateType": "note-draft", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0102", "evidenceDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "privacyTier": "private", + "sourceTrust": "untrusted", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "invalid:untrusted", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-30T12:00:00Z", + "proposal": { "title": "Ineligible source", "summary": "Untrusted input must be rejected before candidate construction." } +} diff --git a/tests/fixtures/foundation-contracts/candidate-valid-memory.json b/tests/fixtures/foundation-contracts/candidate-valid-memory.json new file mode 100644 index 00000000..7c8eae50 --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-valid-memory.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0001", + "candidateType": "memory-unit", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0001", "evidenceDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "privacyTier": "local-private", + "sourceTrust": "user-authored", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "memory-unit:example:0001", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-30T12:00:00Z", + "proposal": { "title": "Example preference", "summary": "A synthetic source-backed memory candidate awaiting governed review." } +} diff --git a/tests/fixtures/foundation-contracts/candidate-valid-project-signal.json b/tests/fixtures/foundation-contracts/candidate-valid-project-signal.json new file mode 100644 index 00000000..c89974ce --- /dev/null +++ b/tests/fixtures/foundation-contracts/candidate-valid-project-signal.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": "jarvos.candidate.v1", + "candidateId": "jarvos:candidate:example:c-0002", + "candidateType": "project-signal", + "authority": "non-authoritative", + "sources": [{ "sourceEventId": "jarvos:source-event:example:se-0002", "evidenceDigest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }], + "privacyTier": "private", + "sourceTrust": "assistant-derived", + "construction": { "extractorId": "example-extractor", "extractorVersion": "1.0.0", "eligibilityPolicyId": "jarvos:policy:example:eligibility-v1" }, + "dedupeKey": "project-signal:example:0002", + "createdAt": "2026-08-31T12:00:00Z", + "expiresAt": "2026-09-07T12:00:00Z", + "proposal": { "title": "Possible project signal", "summary": "A proposal that references evidence without issuing or replacing a Project identity." } +} diff --git a/tests/fixtures/foundation-contracts/identity-invalid-kind.json b/tests/fixtures/foundation-contracts/identity-invalid-kind.json new file mode 100644 index 00000000..6f44b180 --- /dev/null +++ b/tests/fixtures/foundation-contracts/identity-invalid-kind.json @@ -0,0 +1 @@ +"jarvos:machine:example:h-0001" diff --git a/tests/fixtures/foundation-contracts/identity-invalid-path.json b/tests/fixtures/foundation-contracts/identity-invalid-path.json new file mode 100644 index 00000000..be029600 --- /dev/null +++ b/tests/fixtures/foundation-contracts/identity-invalid-path.json @@ -0,0 +1 @@ +"jarvos:artifact:example:notes/path" diff --git a/tests/fixtures/foundation-contracts/identity-valid-kinds.json b/tests/fixtures/foundation-contracts/identity-valid-kinds.json new file mode 100644 index 00000000..0ada2f8b --- /dev/null +++ b/tests/fixtures/foundation-contracts/identity-valid-kinds.json @@ -0,0 +1,13 @@ +{ + "mind": "jarvos:mind:example:m-0001", + "installation": "jarvos:installation:example:i-0001", + "host": "jarvos:host:example:h-0001", + "harness-instance": "jarvos:harness-instance:example:hi-0001", + "session": "jarvos:session:example:s-0001", + "source-event": "jarvos:source-event:example:se-0001", + "candidate": "jarvos:candidate:example:c-0001", + "artifact": "jarvos:artifact:example:a-0001", + "project": "jarvos:project:example:p-0001", + "policy": "jarvos:policy:example:po-0001", + "receipt": "jarvos:receipt:example:r-0001" +} diff --git a/tests/fixtures/foundation-contracts/ledger-invalid-absolute-evidence.json b/tests/fixtures/foundation-contracts/ledger-invalid-absolute-evidence.json new file mode 100644 index 00000000..e27029f4 --- /dev/null +++ b/tests/fixtures/foundation-contracts/ledger-invalid-absolute-evidence.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { "capabilityId": "unsafe-evidence", "title": "Unsafe evidence reference", "specification": "draft", "implementation": "absent", "repository": "local-only", "verification": "untested", "activation": "inactive", "authority": "none", "evidence": [{ "type": "repo-path", "ref": "/private/example/contract.js" }], "assertedOn": "2026-08-31" } + ] +} diff --git a/tests/fixtures/foundation-contracts/ledger-invalid-duplicates.json b/tests/fixtures/foundation-contracts/ledger-invalid-duplicates.json new file mode 100644 index 00000000..31d7d7a7 --- /dev/null +++ b/tests/fixtures/foundation-contracts/ledger-invalid-duplicates.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { "capabilityId": "duplicate", "title": "First", "specification": "draft", "implementation": "absent", "repository": "local-only", "verification": "untested", "activation": "inactive", "authority": "none", "evidence": [{ "type": "document", "ref": "docs/first.md" }], "assertedOn": "2026-08-31" }, + { "capabilityId": "duplicate", "title": "Second", "specification": "draft", "implementation": "absent", "repository": "local-only", "verification": "untested", "activation": "inactive", "authority": "none", "evidence": [{ "type": "document", "ref": "docs/second.md" }], "assertedOn": "2026-08-31" } + ] +} diff --git a/tests/fixtures/foundation-contracts/ledger-invalid-machine-activation.json b/tests/fixtures/foundation-contracts/ledger-invalid-machine-activation.json new file mode 100644 index 00000000..4b399498 --- /dev/null +++ b/tests/fixtures/foundation-contracts/ledger-invalid-machine-activation.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { "capabilityId": "machine-state", "title": "Machine-specific activation", "specification": "canonical", "implementation": "complete", "repository": "merged", "verification": "clean-install-proven", "activation": "host-alpha", "authority": "active", "evidence": [{ "type": "test", "ref": "tests/activation.test.js" }], "assertedOn": "2026-08-31" } + ] +} diff --git a/tests/fixtures/foundation-contracts/ledger-invalid-schema.json b/tests/fixtures/foundation-contracts/ledger-invalid-schema.json new file mode 100644 index 00000000..e1d1f9c8 --- /dev/null +++ b/tests/fixtures/foundation-contracts/ledger-invalid-schema.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v2", + "records": [ + { "capabilityId": "future-schema", "title": "Unsupported schema", "specification": "draft", "implementation": "absent", "repository": "local-only", "verification": "untested", "activation": "inactive", "authority": "none", "evidence": [{ "type": "document", "ref": "docs/future.md" }], "assertedOn": "2026-08-31" } + ] +} diff --git a/tests/fixtures/foundation-contracts/ledger-valid.json b/tests/fixtures/foundation-contracts/ledger-valid.json new file mode 100644 index 00000000..1dbd1d9c --- /dev/null +++ b/tests/fixtures/foundation-contracts/ledger-valid.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { + "capabilityId": "example-contract", + "title": "Example portable contract", + "specification": "canonical", + "implementation": "partial", + "repository": "local-only", + "verification": "fixture-proven", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "repo-path", "ref": "modules/example/src/contract.js" }, + { "type": "test", "ref": "modules/example/test/contract.test.js" } + ], + "assertedOn": "2026-08-31", + "notes": "Synthetic fixture; it makes no live activation claim." + }, + { + "capabilityId": "example-adapter", + "title": "Example optional adapter", + "specification": "draft", + "implementation": "absent", + "repository": "local-only", + "verification": "untested", + "activation": "unknown", + "authority": "read-only", + "evidence": [{ "type": "document", "ref": "docs/example-adapter.md" }], + "assertedOn": "2026-08-31" + } + ] +} diff --git a/tests/fixtures/foundation-contracts/manifest.json b/tests/fixtures/foundation-contracts/manifest.json new file mode 100644 index 00000000..4ec89d15 --- /dev/null +++ b/tests/fixtures/foundation-contracts/manifest.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": "jarvos.foundation-fixtures.v1", + "cases": [ + { "name": "all portable identity kinds", "contract": "identity-set", "file": "identity-valid-kinds.json", "valid": true }, + { "name": "path-like identity", "contract": "identity", "file": "identity-invalid-path.json", "valid": false }, + { "name": "unknown identity kind", "contract": "identity", "file": "identity-invalid-kind.json", "valid": false }, + { "name": "generic capability ledger", "contract": "capability-ledger", "file": "ledger-valid.json", "valid": true }, + { "name": "duplicate capability IDs", "contract": "capability-ledger", "file": "ledger-invalid-duplicates.json", "valid": false }, + { "name": "absolute evidence path", "contract": "capability-ledger", "file": "ledger-invalid-absolute-evidence.json", "valid": false }, + { "name": "machine-specific activation", "contract": "capability-ledger", "file": "ledger-invalid-machine-activation.json", "valid": false }, + { "name": "unsupported ledger schema", "contract": "capability-ledger", "file": "ledger-invalid-schema.json", "valid": false }, + { "name": "memory candidate", "contract": "candidate", "file": "candidate-valid-memory.json", "valid": true }, + { "name": "project signal candidate", "contract": "candidate", "file": "candidate-valid-project-signal.json", "valid": true }, + { "name": "authoritative candidate", "contract": "candidate", "file": "candidate-invalid-authoritative.json", "valid": false }, + { "name": "untrusted candidate", "contract": "candidate", "file": "candidate-invalid-untrusted.json", "valid": false }, + { "name": "candidate missing expiry", "contract": "candidate", "file": "candidate-invalid-missing-expiry.json", "valid": false }, + { "name": "candidate with bad digest", "contract": "candidate", "file": "candidate-invalid-bad-digest.json", "valid": false }, + { "name": "candidate with mutable status", "contract": "candidate", "file": "candidate-invalid-status.json", "valid": false }, + { "name": "committed promotion", "contract": "promotion-receipt", "file": "receipt-valid-committed.json", "valid": true }, + { "name": "failed promotion attempt", "contract": "promotion-receipt", "file": "receipt-valid-failed.json", "valid": true }, + { "name": "supersession with predecessor", "contract": "promotion-receipt", "file": "receipt-valid-supersession.json", "valid": true }, + { "name": "promotion without candidate", "contract": "promotion-receipt", "file": "receipt-invalid-no-candidate.json", "valid": false }, + { "name": "supersession without predecessor", "contract": "promotion-receipt", "file": "receipt-invalid-no-predecessor.json", "valid": false }, + { "name": "failed attempt inventing revision", "contract": "promotion-receipt", "file": "receipt-invalid-invented-revision.json", "valid": false }, + { "name": "receipt with wrong identity kinds", "contract": "promotion-receipt", "file": "receipt-invalid-identity-kinds.json", "valid": false }, + { "name": "receipt with raw content field", "contract": "promotion-receipt", "file": "receipt-invalid-unknown-field.json", "valid": false } + ] +} diff --git a/tests/fixtures/foundation-contracts/receipt-invalid-identity-kinds.json b/tests/fixtures/foundation-contracts/receipt-invalid-identity-kinds.json new file mode 100644 index 00000000..50fd57f7 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-invalid-identity-kinds.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:candidate:example:r-0104", + "operation": "promotion", + "outcome": "committed", + "candidateIds": ["jarvos:artifact:example:c-0104"], + "policyId": "jarvos:receipt:example:p-0104", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "projects", "artifactId": "jarvos:project:example:a-0104", "revisionBefore": null, "revisionAfter": "rev-0104", "reversalMode": "none" }, + "recordedAt": "2026-08-31T13:15:00Z", + "evidence": [{ "type": "verification", "ref": "verification:projects:0104", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-invalid-invented-revision.json b/tests/fixtures/foundation-contracts/receipt-invalid-invented-revision.json new file mode 100644 index 00000000..e25e041b --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-invalid-invented-revision.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0103", + "operation": "promotion", + "outcome": "failed", + "candidateIds": ["jarvos:candidate:example:c-0103"], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "ontology", "artifactId": "jarvos:artifact:example:a-0103", "revisionBefore": "rev-0001", "revisionAfter": "invented-revision", "reversalMode": "rollback" }, + "recordedAt": "2026-08-31T13:10:00Z", + "evidence": [{ "type": "verification", "ref": "verification:ontology:0103", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-invalid-no-candidate.json b/tests/fixtures/foundation-contracts/receipt-invalid-no-candidate.json new file mode 100644 index 00000000..1f73b904 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-invalid-no-candidate.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0101", + "operation": "promotion", + "outcome": "committed", + "candidateIds": [], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "memory", "artifactId": "jarvos:artifact:example:a-0101", "revisionBefore": null, "revisionAfter": "rev-0101", "reversalMode": "none" }, + "recordedAt": "2026-08-31T13:00:00Z", + "evidence": [{ "type": "verification", "ref": "verification:memory:0101", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-invalid-no-predecessor.json b/tests/fixtures/foundation-contracts/receipt-invalid-no-predecessor.json new file mode 100644 index 00000000..c3d81d27 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-invalid-no-predecessor.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0102", + "operation": "rollback", + "outcome": "committed", + "candidateIds": [], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "journal", "artifactId": "jarvos:artifact:example:a-0102", "revisionBefore": "rev-0002", "revisionAfter": "rev-0003", "reversalMode": "none" }, + "recordedAt": "2026-08-31T13:05:00Z", + "evidence": [{ "type": "verification", "ref": "verification:journal:0102", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-invalid-unknown-field.json b/tests/fixtures/foundation-contracts/receipt-invalid-unknown-field.json new file mode 100644 index 00000000..70a01f5e --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-invalid-unknown-field.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0105", + "operation": "promotion", + "outcome": "committed", + "candidateIds": ["jarvos:candidate:example:c-0105"], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "work", "artifactId": "jarvos:artifact:example:a-0105", "revisionBefore": null, "revisionAfter": "rev-0105", "reversalMode": "none" }, + "recordedAt": "2026-08-31T13:20:00Z", + "evidence": [{ "type": "verification", "ref": "verification:work:0105", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + "rawContent": "must never be carried by a receipt" +} diff --git a/tests/fixtures/foundation-contracts/receipt-valid-committed.json b/tests/fixtures/foundation-contracts/receipt-valid-committed.json new file mode 100644 index 00000000..49f0a0c6 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-valid-committed.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0001", + "operation": "promotion", + "outcome": "committed", + "candidateIds": ["jarvos:candidate:example:c-0001"], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "memory", "artifactId": "jarvos:artifact:example:a-0001", "revisionBefore": null, "revisionAfter": "rev-0001", "reversalMode": "supersession" }, + "recordedAt": "2026-08-31T12:00:00Z", + "evidence": [{ "type": "verification", "ref": "verification:memory:0001", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-valid-failed.json b/tests/fixtures/foundation-contracts/receipt-valid-failed.json new file mode 100644 index 00000000..f4c63238 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-valid-failed.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0002", + "operation": "promotion", + "outcome": "failed", + "candidateIds": ["jarvos:candidate:example:c-0002"], + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "policy-automatic" }, + "destination": { "surface": "notes", "artifactId": "jarvos:artifact:example:a-0002", "revisionBefore": "rev-0001", "revisionAfter": null, "reversalMode": "none" }, + "recordedAt": "2026-08-31T12:05:00Z", + "evidence": [{ "type": "policy-decision", "ref": "policy-decision:0002", "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }] +} diff --git a/tests/fixtures/foundation-contracts/receipt-valid-supersession.json b/tests/fixtures/foundation-contracts/receipt-valid-supersession.json new file mode 100644 index 00000000..2bd01319 --- /dev/null +++ b/tests/fixtures/foundation-contracts/receipt-valid-supersession.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "jarvos.promotion-receipt.v1", + "receiptId": "jarvos:receipt:example:r-0003", + "operation": "supersession", + "outcome": "committed", + "candidateIds": [], + "predecessorReceiptId": "jarvos:receipt:example:r-0001", + "policyId": "jarvos:policy:example:promotion-v1", + "authorization": { "mode": "user-reviewed" }, + "destination": { "surface": "memory", "artifactId": "jarvos:artifact:example:a-0001", "revisionBefore": "rev-0001", "revisionAfter": "rev-0002", "reversalMode": "none" }, + "recordedAt": "2026-08-31T12:10:00Z", + "evidence": [{ "type": "destination-receipt", "ref": "destination-receipt:0003", "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" }] +} diff --git a/tests/secondbrain-external-integrations-doc-test.js b/tests/secondbrain-external-integrations-doc-test.js index 68aafb7b..9dea50de 100644 --- a/tests/secondbrain-external-integrations-doc-test.js +++ b/tests/secondbrain-external-integrations-doc-test.js @@ -62,7 +62,11 @@ test('secondbrain external integration inventory preserves authority boundaries' 'not public-core, not durable truth, not live task state', 'must not auto-promote into GBrain, Vault notes, Paperclip, ontology, or durable memory', 'Not production-integrated', - 'No automatic ingestion of every AI conversation', + 'No indiscriminate ingestion of every AI conversation', + 'future eligible-interaction adapter may produce bounded non-authoritative candidates', + 'this foundation contains no source adapter that does so', + 'destination-owned promotion plus outcome receipt', + 'does not automatically believe, promote, or retain every conversation', 'explicit configured directory and valid IANA timezone', 'creates only a missing current-date file', 'health is read-only, ensure is an empty-input today-only action',