From 2f5846d5133518ed52f1db3388eac3ed2cf59954 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 15:57:58 -0400 Subject: [PATCH 01/11] docs: add product realization foundation plan --- ...-product-realization-foundation-v1-plan.md | 535 ++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md 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..bf02fcde --- /dev/null +++ b/docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md @@ -0,0 +1,535 @@ +# Product realization foundation v1 plan + +Status: approved for local repository-only execution + +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 Mac Mini mutation. + +## Goal + +Add four fail-closed, machine-readable foundation contracts and the minimum +canonical 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 canonical 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: repository `local-only`, 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`. + +Canonical 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 +Andrew'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. Specification is `canonical` after the +contract docs land. Verification is `fixture-proven`; repository is +`local-only`; activation is `inactive`; authority is `none`. + +## Candidate envelope contract + +Schema version: `jarvos.candidate.v1`. + +The envelope is immutable. It 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 immutable candidate validator, 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 — Canonical 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 `local-only`, `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. From bf49cb214d7d70aef5cc7c9604e527cc4ca286bf Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:09:40 -0400 Subject: [PATCH 02/11] feat(control-plane): add portable identity contract --- modules/jarvos-control-plane/README.md | 22 +++++ modules/jarvos-control-plane/package.json | 3 +- modules/jarvos-control-plane/src/identity.js | 95 +++++++++++++++++++ modules/jarvos-control-plane/src/index.js | 2 + .../test/identity.test.js | 82 ++++++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 modules/jarvos-control-plane/src/identity.js create mode 100644 modules/jarvos-control-plane/test/identity.test.js diff --git a/modules/jarvos-control-plane/README.md b/modules/jarvos-control-plane/README.md index 94f88878..8274b0a3 100644 --- a/modules/jarvos-control-plane/README.md +++ b/modules/jarvos-control-plane/README.md @@ -41,6 +41,28 @@ 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. + ## 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..31c5e6f9 100644 --- a/modules/jarvos-control-plane/package.json +++ b/modules/jarvos-control-plane/package.json @@ -13,7 +13,8 @@ "./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" }, "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..aaa3ca2c 100644 --- a/modules/jarvos-control-plane/src/index.js +++ b/modules/jarvos-control-plane/src/index.js @@ -8,6 +8,7 @@ const storage = require('./storage'); const applicationService = require('./application-service'); const protectedResource = require('./protected-resource'); const schedulerAdapter = require('./scheduler-adapter'); +const identity = require('./identity'); module.exports = { ...contracts, @@ -18,4 +19,5 @@ module.exports = { ...applicationService, ...protectedResource, ...schedulerAdapter, + ...identity, }; 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'); +}); From 8303902401f5aa9b0ad82588c382e9e3b0afb89e Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:09:40 -0400 Subject: [PATCH 03/11] feat: add capability truth ledger contract --- capability-ledger.json | 69 +++++++++ docs/architecture/capability-truth-ledger.md | 58 +++++++ scripts/lib/capability-ledger.js | 154 +++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 capability-ledger.json create mode 100644 docs/architecture/capability-truth-ledger.md create mode 100644 scripts/lib/capability-ledger.js diff --git a/capability-ledger.json b/capability-ledger.json new file mode 100644 index 00000000..4529d13a --- /dev/null +++ b/capability-ledger.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": "jarvos.capability-ledger.v1", + "records": [ + { + "capabilityId": "portable-identity", + "title": "Portable opaque identity contract", + "specification": "canonical", + "implementation": "partial", + "repository": "local-only", + "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" } + ], + "assertedOn": "2026-08-31", + "notes": "Validator, exports, and unit tests exist; no product consumer issues identifiers yet." + }, + { + "capabilityId": "capability-truth-ledger", + "title": "Capability truth ledger contract", + "specification": "canonical", + "implementation": "partial", + "repository": "local-only", + "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" } + ], + "assertedOn": "2026-08-31", + "notes": "This ledger validates against its own contract; no automated reporter consumes it yet." + }, + { + "capabilityId": "candidate-envelope", + "title": "Non-authoritative candidate envelope contract", + "specification": "draft", + "implementation": "absent", + "repository": "local-only", + "verification": "untested", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "document", "ref": "docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md" } + ], + "assertedOn": "2026-08-31", + "notes": "Planned in a later slice of this branch; no validator or fixtures exist yet." + }, + { + "capabilityId": "promotion-receipt", + "title": "Cross-surface promotion receipt envelope contract", + "specification": "draft", + "implementation": "absent", + "repository": "local-only", + "verification": "untested", + "activation": "inactive", + "authority": "none", + "evidence": [ + { "type": "document", "ref": "docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md" } + ], + "assertedOn": "2026-08-31", + "notes": "Planned in a later slice of this branch; no validator or fixtures exist yet." + } + ] +} diff --git a/docs/architecture/capability-truth-ledger.md b/docs/architecture/capability-truth-ledger.md new file mode 100644 index 00000000..dccef9c0 --- /dev/null +++ b/docs/architecture/capability-truth-ledger.md @@ -0,0 +1,58 @@ +# 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. + +## 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: 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 this local foundation +slice. Contracts that already exist are asserted at `implementation: partial` +and `verification: fixture-proven`; contracts that are only planned stay at +`specification: draft`, `implementation: absent`, and `verification: untested` +until their code and fixtures land. Every seed record is `repository: +local-only`, `activation: inactive`, and `authority: none`. diff --git a/scripts/lib/capability-ledger.js b/scripts/lib/capability-ledger.js new file mode 100644 index 00000000..a17e42c6 --- /dev/null +++ b/scripts/lib/capability-ledger.js @@ -0,0 +1,154 @@ +'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 { + record.evidence.forEach((entry, evidenceIndex) => validateEvidenceEntry(entry, 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(); + ledger.records.forEach((record, index) => validateRecord(record, 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, +}; From c6c3bc718eec51e57ce0ac6d152d2513709ade66 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:19:07 -0400 Subject: [PATCH 04/11] feat(ambient): add candidate envelope contract --- .../docs/contracts/CANDIDATE_ENVELOPE.md | 71 ++++++ .../packages/jarvos-ambient/package.json | 3 +- .../src/intent/candidate-contract.js | 225 ++++++++++++++++++ .../jarvos-ambient/src/intent/index.js | 1 + .../test/candidate-contract.test.js | 155 ++++++++++++ 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md create mode 100644 modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js create mode 100644 modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js 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..94fccd58 --- /dev/null +++ b/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md @@ -0,0 +1,71 @@ +# 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 + +A candidate is an immutable object with exactly these fields. 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 + +- The 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..7912d01b --- /dev/null +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js @@ -0,0 +1,225 @@ +'use strict'; + +// Immutable, non-authoritative candidate envelope for the ambient intent layer. +// A candidate is a bounded 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. 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 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; + const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(); + if (day > daysInMonth) return null; + if (offsetSign && (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 59)) 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 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(); + candidate.sources.forEach((entry, index) => validateSourceEntry(entry, 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 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..0b0d31af --- /dev/null +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js @@ -0,0 +1,155 @@ +'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 without mutation powers', () => { + for (const candidateType of contract.CANDIDATE_TYPES) { + const candidate = makeCandidate({ candidateType }); + assert.deepEqual(contract.validateCandidate(candidate), [], candidateType); + assert.equal(contract.assertCandidate(candidate), candidate); + } +}); + +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); +}); + +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); +}); + +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/); +}); From 15cda241530272ac1534e1c62af2c6f5f1ce45cd Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:39:25 -0400 Subject: [PATCH 05/11] feat(control-plane): add promotion receipt contract --- modules/jarvos-control-plane/README.md | 18 ++ modules/jarvos-control-plane/package.json | 3 +- modules/jarvos-control-plane/src/index.js | 2 + .../src/promotion-receipt.js | 233 ++++++++++++++++++ .../test/promotion-receipt.test.js | 159 ++++++++++++ 5 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 modules/jarvos-control-plane/src/promotion-receipt.js create mode 100644 modules/jarvos-control-plane/test/promotion-receipt.test.js diff --git a/modules/jarvos-control-plane/README.md b/modules/jarvos-control-plane/README.md index 8274b0a3..74c1ca8b 100644 --- a/modules/jarvos-control-plane/README.md +++ b/modules/jarvos-control-plane/README.md @@ -63,6 +63,24 @@ 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 31c5e6f9..e603f9c5 100644 --- a/modules/jarvos-control-plane/package.json +++ b/modules/jarvos-control-plane/package.json @@ -14,7 +14,8 @@ "./application-service": "./src/application-service.js", "./manager": "./scripts/jarvos-manager.js", "./protected-resource": "./src/protected-resource/index.js", - "./identity": "./src/identity.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/index.js b/modules/jarvos-control-plane/src/index.js index aaa3ca2c..044c0a3a 100644 --- a/modules/jarvos-control-plane/src/index.js +++ b/modules/jarvos-control-plane/src/index.js @@ -9,6 +9,7 @@ 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, @@ -20,4 +21,5 @@ module.exports = { ...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..fab7a0e1 --- /dev/null +++ b/modules/jarvos-control-plane/src/promotion-receipt.js @@ -0,0 +1,233 @@ +'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 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 > new Date(Date.UTC(year, month, 0)).getUTCDate()) 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(); + receipt.candidateIds.forEach((candidateId, 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 { + receipt.evidence.forEach((entry, index) => validateEvidence(entry, 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/promotion-receipt.test.js b/modules/jarvos-control-plane/test/promotion-receipt.test.js new file mode 100644 index 00000000..d0c2b3b5 --- /dev/null +++ b/modules/jarvos-control-plane/test/promotion-receipt.test.js @@ -0,0 +1,159 @@ +'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({ + 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); + 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); +}); + +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/); +}); From c21640f41cb120d86f92b0edf598a206f49461d7 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:49:34 -0400 Subject: [PATCH 06/11] test: add foundation contract conformance fixtures --- .../foundation-contracts-conformance.test.js | 101 ++++++++++++++++++ .../candidate-invalid-authoritative.json | 14 +++ .../candidate-invalid-bad-digest.json | 14 +++ .../candidate-invalid-missing-expiry.json | 13 +++ .../candidate-invalid-status.json | 15 +++ .../candidate-invalid-untrusted.json | 14 +++ .../candidate-valid-memory.json | 14 +++ .../candidate-valid-project-signal.json | 14 +++ .../identity-invalid-kind.json | 1 + .../identity-invalid-path.json | 1 + .../identity-valid-kinds.json | 13 +++ .../ledger-invalid-absolute-evidence.json | 6 ++ .../ledger-invalid-duplicates.json | 7 ++ .../ledger-invalid-machine-activation.json | 6 ++ .../ledger-invalid-schema.json | 6 ++ .../foundation-contracts/ledger-valid.json | 33 ++++++ .../foundation-contracts/manifest.json | 28 +++++ .../receipt-invalid-identity-kinds.json | 12 +++ .../receipt-invalid-invented-revision.json | 12 +++ .../receipt-invalid-no-candidate.json | 12 +++ .../receipt-invalid-no-predecessor.json | 12 +++ .../receipt-invalid-unknown-field.json | 13 +++ .../receipt-valid-committed.json | 12 +++ .../receipt-valid-failed.json | 12 +++ .../receipt-valid-supersession.json | 13 +++ 25 files changed, 398 insertions(+) create mode 100644 modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js create mode 100644 tests/fixtures/foundation-contracts/candidate-invalid-authoritative.json create mode 100644 tests/fixtures/foundation-contracts/candidate-invalid-bad-digest.json create mode 100644 tests/fixtures/foundation-contracts/candidate-invalid-missing-expiry.json create mode 100644 tests/fixtures/foundation-contracts/candidate-invalid-status.json create mode 100644 tests/fixtures/foundation-contracts/candidate-invalid-untrusted.json create mode 100644 tests/fixtures/foundation-contracts/candidate-valid-memory.json create mode 100644 tests/fixtures/foundation-contracts/candidate-valid-project-signal.json create mode 100644 tests/fixtures/foundation-contracts/identity-invalid-kind.json create mode 100644 tests/fixtures/foundation-contracts/identity-invalid-path.json create mode 100644 tests/fixtures/foundation-contracts/identity-valid-kinds.json create mode 100644 tests/fixtures/foundation-contracts/ledger-invalid-absolute-evidence.json create mode 100644 tests/fixtures/foundation-contracts/ledger-invalid-duplicates.json create mode 100644 tests/fixtures/foundation-contracts/ledger-invalid-machine-activation.json create mode 100644 tests/fixtures/foundation-contracts/ledger-invalid-schema.json create mode 100644 tests/fixtures/foundation-contracts/ledger-valid.json create mode 100644 tests/fixtures/foundation-contracts/manifest.json create mode 100644 tests/fixtures/foundation-contracts/receipt-invalid-identity-kinds.json create mode 100644 tests/fixtures/foundation-contracts/receipt-invalid-invented-revision.json create mode 100644 tests/fixtures/foundation-contracts/receipt-invalid-no-candidate.json create mode 100644 tests/fixtures/foundation-contracts/receipt-invalid-no-predecessor.json create mode 100644 tests/fixtures/foundation-contracts/receipt-invalid-unknown-field.json create mode 100644 tests/fixtures/foundation-contracts/receipt-valid-committed.json create mode 100644 tests/fixtures/foundation-contracts/receipt-valid-failed.json create mode 100644 tests/fixtures/foundation-contracts/receipt-valid-supersession.json 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..ce4ba0b6 --- /dev/null +++ b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js @@ -0,0 +1,101 @@ +'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('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/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" }] +} From b3447f1a1e2d4c99f25aad603ec75dff8b57487b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:51:32 -0400 Subject: [PATCH 07/11] docs: reconcile candidate and truth boundaries --- capability-ledger.json | 26 +++++++++------- docs/architecture/capability-truth-ledger.md | 10 +++---- .../product-category-and-boundaries.md | 30 ++++++++++++++++++- .../secondbrain-external-integrations.md | 20 ++++++++++++- ...ondbrain-external-integrations-doc-test.js | 5 +++- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/capability-ledger.json b/capability-ledger.json index 4529d13a..81fad138 100644 --- a/capability-ledger.json +++ b/capability-ledger.json @@ -38,32 +38,38 @@ { "capabilityId": "candidate-envelope", "title": "Non-authoritative candidate envelope contract", - "specification": "draft", - "implementation": "absent", + "specification": "canonical", + "implementation": "partial", "repository": "local-only", - "verification": "untested", + "verification": "fixture-proven", "activation": "inactive", "authority": "none", "evidence": [ - { "type": "document", "ref": "docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md" } + { "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" } ], "assertedOn": "2026-08-31", - "notes": "Planned in a later slice of this branch; no validator or fixtures exist yet." + "notes": "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": "absent", + "specification": "canonical", + "implementation": "partial", "repository": "local-only", - "verification": "untested", + "verification": "fixture-proven", "activation": "inactive", "authority": "none", "evidence": [ - { "type": "document", "ref": "docs/plans/2026-08-31-feat-product-realization-foundation-v1-plan.md" } + { "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" } ], "assertedOn": "2026-08-31", - "notes": "Planned in a later slice of this branch; no validator or fixtures exist yet." + "notes": "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 index dccef9c0..91bbe6b5 100644 --- a/docs/architecture/capability-truth-ledger.md +++ b/docs/architecture/capability-truth-ledger.md @@ -51,8 +51,8 @@ traversal, and cannot carry a `file:` or other URI scheme. ## Seed ledger The tracked `capability-ledger.json` seed describes only this local foundation -slice. Contracts that already exist are asserted at `implementation: partial` -and `verification: fixture-proven`; contracts that are only planned stay at -`specification: draft`, `implementation: absent`, and `verification: untested` -until their code and fixtures land. Every seed record is `repository: -local-only`, `activation: inactive`, and `authority: none`. +slice. Its identity, truth-ledger, candidate-envelope, and promotion-receipt +contracts are asserted at `implementation: partial` and `verification: +fixture-proven` because their validators and tests exist but no installed +product consumer activates them. Every seed record is `repository: local-only`, +`activation: inactive`, and `authority: none`. diff --git a/docs/architecture/product-category-and-boundaries.md b/docs/architecture/product-category-and-boundaries.md index f3116612..edfd1690 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,34 @@ 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. +## Product Realization Boundary + +jarvOS may autonomously observe eligible AI interactions and prepare bounded, +source-backed candidates for notes, journals, memory, ontology, Projects, +skills, or work. That is how the product can construct a useful digital twin +across harnesses without treating every transcript or model statement as true. + +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; +- a 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..d52c422e 100644 --- a/docs/architecture/secondbrain-external-integrations.md +++ b/docs/architecture/secondbrain-external-integrations.md @@ -102,12 +102,28 @@ The active secondbrain path is: ```text intentional capture -> Obsidian-compatible Markdown Notes/Journal + +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 proposal path: it may autonomously prepare candidates, but 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 +163,9 @@ 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. Eligible interactions 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/tests/secondbrain-external-integrations-doc-test.js b/tests/secondbrain-external-integrations-doc-test.js index 68aafb7b..1a2c41b0 100644 --- a/tests/secondbrain-external-integrations-doc-test.js +++ b/tests/secondbrain-external-integrations-doc-test.js @@ -62,7 +62,10 @@ 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', + 'Eligible interactions may produce bounded non-authoritative candidates', + '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', From 2b1d31f083fcf28f3eb016b39e7b672274dd5f62 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 16:56:42 -0400 Subject: [PATCH 08/11] docs: keep foundation plan host-generic --- ...2026-08-31-feat-product-realization-foundation-v1-plan.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index bf02fcde..775e8967 100644 --- 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 @@ -10,7 +10,8 @@ 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 Mac Mini mutation. +activation, scheduler, runtime change, Vault operation, or private host +mutation. ## Goal @@ -175,7 +176,7 @@ Every record contains: - optional bounded `notes`. The activation vocabulary is generic public-product language. It must not name -Andrew's machines. +an operator's machines. Invariants: From 8d41a2dd055e9aa60b043da8e9ce1aff62c0d775 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 17:01:38 -0400 Subject: [PATCH 09/11] fix: harden foundation timestamp validation --- .../src/promotion-receipt.js | 10 +++++++++- .../test/promotion-receipt.test.js | 3 +++ .../src/intent/candidate-contract.js | 17 ++++++++++++++--- .../test/candidate-contract.test.js | 6 ++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/modules/jarvos-control-plane/src/promotion-receipt.js b/modules/jarvos-control-plane/src/promotion-receipt.js index fab7a0e1..5fac6c9d 100644 --- a/modules/jarvos-control-plane/src/promotion-receipt.js +++ b/modules/jarvos-control-plane/src/promotion-receipt.js @@ -64,6 +64,14 @@ function isLogicalRef(value) { && !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); @@ -76,7 +84,7 @@ function isRealUtcInstant(value) { 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 > new Date(Date.UTC(year, month, 0)).getUTCDate()) return false; + if (day > daysInMonth(year, month)) return false; return !Number.isNaN(Date.parse(value)); } diff --git a/modules/jarvos-control-plane/test/promotion-receipt.test.js b/modules/jarvos-control-plane/test/promotion-receipt.test.js index d0c2b3b5..460c6447 100644 --- a/modules/jarvos-control-plane/test/promotion-receipt.test.js +++ b/modules/jarvos-control-plane/test/promotion-receipt.test.js @@ -149,6 +149,9 @@ test('timestamps must be real ISO UTC instants', () => { '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', () => { 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 index 7912d01b..4d3fc029 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js @@ -74,6 +74,14 @@ function isIdentityOfKind(value, kind) { 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); @@ -87,9 +95,12 @@ function isoInstantMs(value) { const minute = Number(minuteText); const second = Number(secondText); if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59) return null; - const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(); - if (day > daysInMonth) return null; - if (offsetSign && (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 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; } 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 index 0b0d31af..d4c03bf4 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js @@ -145,6 +145,12 @@ test('timestamps must be real ISO instants with expiry after creation', () => { 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', () => { From 34192f11dbc24c0ee262cc30ba98593781b2d442 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 21:50:46 -0400 Subject: [PATCH 10/11] docs: clarify foundation truth and harness boundaries --- capability-ledger.json | 36 +++++++++-------- docs/architecture/capability-truth-ledger.md | 23 ++++++----- .../product-category-and-boundaries.md | 32 +++++++++++++++ .../foundation-contracts-conformance.test.js | 40 +++++++++++++++++++ 4 files changed, 106 insertions(+), 25 deletions(-) diff --git a/capability-ledger.json b/capability-ledger.json index 81fad138..e3dcab53 100644 --- a/capability-ledger.json +++ b/capability-ledger.json @@ -4,43 +4,45 @@ { "capabilityId": "portable-identity", "title": "Portable opaque identity contract", - "specification": "canonical", + "specification": "draft", "implementation": "partial", - "repository": "local-only", + "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": "document", "ref": "docs/architecture/capability-truth-ledger.md" }, + { "type": "pull-request", "ref": "pull/257" } ], "assertedOn": "2026-08-31", - "notes": "Validator, exports, and unit tests exist; no product consumer issues identifiers yet." + "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": "canonical", + "specification": "draft", "implementation": "partial", - "repository": "local-only", + "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": "repo-path", "ref": "capability-ledger.json" }, + { "type": "pull-request", "ref": "pull/257" } ], "assertedOn": "2026-08-31", - "notes": "This ledger validates against its own contract; no automated reporter consumes it yet." + "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": "canonical", + "specification": "draft", "implementation": "partial", - "repository": "local-only", + "repository": "draft-pr", "verification": "fixture-proven", "activation": "inactive", "authority": "none", @@ -48,17 +50,18 @@ { "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": "test", "ref": "tests/fixtures/foundation-contracts/manifest.json" }, + { "type": "pull-request", "ref": "pull/257" } ], "assertedOn": "2026-08-31", - "notes": "Validator, exports, unit tests, and shared conformance fixtures exist; no source adapter or promotion consumer constructs candidates yet." + "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": "canonical", + "specification": "draft", "implementation": "partial", - "repository": "local-only", + "repository": "draft-pr", "verification": "fixture-proven", "activation": "inactive", "authority": "none", @@ -66,10 +69,11 @@ { "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": "test", "ref": "tests/fixtures/foundation-contracts/manifest.json" }, + { "type": "pull-request", "ref": "pull/257" } ], "assertedOn": "2026-08-31", - "notes": "Validator, exports, unit tests, and shared conformance fixtures exist; no destination writer emits this envelope yet." + "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 index 91bbe6b5..ae81d867 100644 --- a/docs/architecture/capability-truth-ledger.md +++ b/docs/architecture/capability-truth-ledger.md @@ -5,7 +5,9 @@ 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. +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 @@ -36,8 +38,9 @@ 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: it cannot be absolute, cannot contain `..` -traversal, and cannot carry a `file:` or other URI scheme. +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 @@ -50,9 +53,11 @@ traversal, and cannot carry a `file:` or other URI scheme. ## Seed ledger -The tracked `capability-ledger.json` seed describes only this local foundation -slice. Its identity, truth-ledger, candidate-envelope, and promotion-receipt -contracts are asserted at `implementation: partial` and `verification: -fixture-proven` because their validators and tests exist but no installed -product consumer activates them. Every seed record is `repository: local-only`, -`activation: inactive`, and `authority: none`. +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 edfd1690..68928257 100644 --- a/docs/architecture/product-category-and-boundaries.md +++ b/docs/architecture/product-category-and-boundaries.md @@ -43,6 +43,38 @@ 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 jarvOS may autonomously observe eligible AI interactions and prepare bounded, diff --git a/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js index ce4ba0b6..646db3b2 100644 --- a/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js +++ b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js @@ -83,6 +83,46 @@ test('the tracked capability ledger conforms to the ledger contract', () => { assert.deepEqual(validateCapabilityLedger(ledger), []); }); +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 = [ From 7603ae6c1d0ee9779e7dda9583115b922ab5cd69 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 31 Aug 2026 22:13:16 -0400 Subject: [PATCH 11/11] fix: harden foundation contract truth boundaries --- .../product-category-and-boundaries.md | 16 +++++----- .../secondbrain-external-integrations.md | 16 ++++++---- ...-product-realization-foundation-v1-plan.md | 29 ++++++++++--------- .../src/promotion-receipt.js | 9 ++++-- .../foundation-contracts-conformance.test.js | 9 ++++++ .../test/promotion-receipt.test.js | 2 ++ .../docs/contracts/CANDIDATE_ENVELOPE.md | 7 +++-- .../src/intent/candidate-contract.js | 24 ++++++++++----- .../test/candidate-contract.test.js | 14 +++++++-- scripts/lib/capability-ledger.js | 8 +++-- ...ondbrain-external-integrations-doc-test.js | 3 +- 11 files changed, 92 insertions(+), 45 deletions(-) diff --git a/docs/architecture/product-category-and-boundaries.md b/docs/architecture/product-category-and-boundaries.md index 68928257..a1325dd6 100644 --- a/docs/architecture/product-category-and-boundaries.md +++ b/docs/architecture/product-category-and-boundaries.md @@ -77,19 +77,21 @@ claim adapter, installed, or live behavior. ## Product Realization Boundary -jarvOS may autonomously observe eligible AI interactions and prepare bounded, -source-backed candidates for notes, journals, memory, ontology, Projects, -skills, or work. That is how the product can construct a useful digital twin -across harnesses without treating every transcript or model statement as true. +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; -- a candidate is immutable and non-authoritative, contains evidence pointers - rather than raw source content, and cannot claim completion, verification, - destination state, or Project identity; +- 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 diff --git a/docs/architecture/secondbrain-external-integrations.md b/docs/architecture/secondbrain-external-integrations.md index d52c422e..beea8f30 100644 --- a/docs/architecture/secondbrain-external-integrations.md +++ b/docs/architecture/secondbrain-external-integrations.md @@ -97,13 +97,15 @@ 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 -eligible AI interactions +future eligible AI interactions -> eligibility and trust boundary -> immutable, source-backed, non-authoritative candidates -> governed review or policy admission @@ -117,8 +119,9 @@ authoritative durable surfaces ``` Intentional capture remains the direct authored path. Eligible ambient -observation is a proposal path: it may autonomously prepare candidates, but it -does not automatically believe, promote, or retain every conversation. Source +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, @@ -164,8 +167,9 @@ Active integrations should be proven through these public-safe signals: ## Non-Goals - No indiscriminate ingestion of every AI conversation, raw transcript - hoarding, or automatic belief. Eligible interactions may produce bounded - non-authoritative candidates; durable promotion remains governed. + 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 index 775e8967..f9047e8e 100644 --- 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 @@ -1,6 +1,6 @@ # Product realization foundation v1 plan -Status: approved for local repository-only execution +Status: implemented as a draft-PR contract slice; no runtime behavior is authorized Base: `af427a68bbe88c0f08dc516356e3fee55cf41b89` (`origin/main`) @@ -16,7 +16,7 @@ mutation. ## Goal Add four fail-closed, machine-readable foundation contracts and the minimum -canonical documentation needed to describe them: +product-boundary documentation needed to describe them: 1. a capability truth ledger that separates specification, implementation, repository, verification, activation, and authority state; @@ -40,7 +40,7 @@ attempt. Documentation reconciliation is limited to: -- the canonical product-boundary document; and +- the product-boundary document; and - the external-integration document's intentional-capture and non-goal language. @@ -58,7 +58,7 @@ rewrite remains separate. 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: repository `local-only`, activation `inactive`, authority `none`, + 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. @@ -114,7 +114,7 @@ replacement. Schema version: `jarvos.identity.v1`. -Canonical identifier grammar: +Identifier grammar: ```text jarvos::: @@ -189,16 +189,17 @@ Invariants: - 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. Specification is `canonical` after the -contract docs land. Verification is `fixture-proven`; repository is -`local-only`; activation is `inactive`; authority is `none`. +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`. -The envelope is immutable. It has no mutable `status` and cannot itself record -promotion, rejection, completion, or recall. +`assertCandidate()` returns an immutable deep clone. The envelope has no +mutable `status` and cannot itself record promotion, rejection, completion, or +recall. Required fields: @@ -388,8 +389,8 @@ Also prove one malformed in-memory ledger is rejected. ### Step 4 — Candidate envelope -Implement the immutable candidate validator, ambient exports, tests, and -contract documentation. +Implement the candidate validator, immutable assertion result, ambient exports, +tests, and contract documentation. Verification: @@ -422,7 +423,7 @@ node --test modules/jarvos-control-plane/test/foundation-contracts-conformance.t node --test modules/jarvos-control-plane/test/*.test.js ``` -### Step 7 — Canonical documentation reconciliation +### 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 @@ -527,7 +528,7 @@ the other branches. 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 `local-only`, `inactive`, `none`, `partial`, and +- [ ] 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 diff --git a/modules/jarvos-control-plane/src/promotion-receipt.js b/modules/jarvos-control-plane/src/promotion-receipt.js index 5fac6c9d..59cccfe0 100644 --- a/modules/jarvos-control-plane/src/promotion-receipt.js +++ b/modules/jarvos-control-plane/src/promotion-receipt.js @@ -180,7 +180,8 @@ function validatePromotionReceipt(receipt) { errors.push('candidateIds must be a bounded array'); } else { const seen = new Set(); - receipt.candidateIds.forEach((candidateId, index) => { + 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)) { @@ -188,7 +189,7 @@ function validatePromotionReceipt(receipt) { } else { seen.add(candidateId); } - }); + } } if (receipt.operation === 'promotion') { if (!Array.isArray(receipt.candidateIds) || receipt.candidateIds.length === 0) { @@ -215,7 +216,9 @@ function validatePromotionReceipt(receipt) { 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 { - receipt.evidence.forEach((entry, index) => validateEvidence(entry, index, errors)); + for (let index = 0; index < receipt.evidence.length; index += 1) { + validateEvidence(receipt.evidence[index], index, errors); + } } return errors; } diff --git a/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js index 646db3b2..c8616af9 100644 --- a/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js +++ b/modules/jarvos-control-plane/test/foundation-contracts-conformance.test.js @@ -83,6 +83,15 @@ test('the tracked capability ledger conforms to the ledger contract', () => { 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) { diff --git a/modules/jarvos-control-plane/test/promotion-receipt.test.js b/modules/jarvos-control-plane/test/promotion-receipt.test.js index 460c6447..dd66fa84 100644 --- a/modules/jarvos-control-plane/test/promotion-receipt.test.js +++ b/modules/jarvos-control-plane/test/promotion-receipt.test.js @@ -78,6 +78,7 @@ test('non-committed outcomes report no mutation or reversal claim', () => { 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); @@ -128,6 +129,7 @@ test('unknown fields and enums fail closed at every nesting level', () => { 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 }], diff --git a/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md b/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md index 94fccd58..140e3892 100644 --- a/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md +++ b/modules/jarvos-secondbrain/docs/contracts/CANDIDATE_ENVELOPE.md @@ -16,8 +16,9 @@ constructed and is therefore not representable in this envelope. ## Shape -A candidate is an immutable object with exactly these fields. Any unknown -top-level or nested field fails closed. +`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 | | --- | --- | @@ -46,7 +47,7 @@ content, recall text, completion output, or destination. ## Invariants -- The envelope is immutable and has no mutable `status`. +- 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 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 index 4d3fc029..27112353 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/src/intent/candidate-contract.js @@ -1,10 +1,11 @@ 'use strict'; -// Immutable, non-authoritative candidate envelope for the ambient intent layer. -// A candidate is a bounded 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. It never carries raw transcript, -// note content, recall text, completion output, or a destination. +// 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'; @@ -152,6 +153,13 @@ function validateProposal(proposal, errors) { } } +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)) { @@ -191,7 +199,9 @@ function validateCandidate(candidate) { errors.push('candidate sources must be a bounded non-empty array'); } else { const seen = new Set(); - candidate.sources.forEach((entry, index) => validateSourceEntry(entry, index, seen, errors)); + for (let index = 0; index < candidate.sources.length; index += 1) { + validateSourceEntry(candidate.sources[index], index, seen, errors); + } } if (!isPlainObject(candidate.construction)) { @@ -222,7 +232,7 @@ function assertCandidate(candidate) { if (errors.length > 0) { throw new Error(`invalid jarvos candidate: ${errors.join('; ')}`); } - return candidate; + return deepFreeze(structuredClone(candidate)); } module.exports = { 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 index d4c03bf4..e32ed72f 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js +++ b/modules/jarvos-secondbrain/packages/jarvos-ambient/test/candidate-contract.test.js @@ -63,11 +63,20 @@ test('the module exposes no recall, completion, write, store, or promotion funct ]); }); -test('valid candidate types assert without mutation powers', () => { +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); - assert.equal(contract.assertCandidate(candidate), candidate); + 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); } }); @@ -112,6 +121,7 @@ test('sources must be unique evidence pointers with source-event identities and 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', () => { diff --git a/scripts/lib/capability-ledger.js b/scripts/lib/capability-ledger.js index a17e42c6..55626542 100644 --- a/scripts/lib/capability-ledger.js +++ b/scripts/lib/capability-ledger.js @@ -101,7 +101,9 @@ function validateRecord(record, index, seenIds, errors) { 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 { - record.evidence.forEach((entry, evidenceIndex) => validateEvidenceEntry(entry, evidenceIndex, label, errors)); + 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`); @@ -128,7 +130,9 @@ function validateCapabilityLedger(ledger) { return errors; } const seenIds = new Set(); - ledger.records.forEach((record, index) => validateRecord(record, index, seenIds, errors)); + for (let index = 0; index < ledger.records.length; index += 1) { + validateRecord(ledger.records[index], index, seenIds, errors); + } return errors; } diff --git a/tests/secondbrain-external-integrations-doc-test.js b/tests/secondbrain-external-integrations-doc-test.js index 1a2c41b0..9dea50de 100644 --- a/tests/secondbrain-external-integrations-doc-test.js +++ b/tests/secondbrain-external-integrations-doc-test.js @@ -63,7 +63,8 @@ test('secondbrain external integration inventory preserves authority boundaries' 'must not auto-promote into GBrain, Vault notes, Paperclip, ontology, or durable memory', 'Not production-integrated', 'No indiscriminate ingestion of every AI conversation', - 'Eligible interactions may produce bounded non-authoritative candidates', + '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',