Skip to content

feat(spec,demand-intel): kit-demand signal and fixed periodic public release layer (R44, PX-13) - #365

Merged
LamaSu merged 5 commits into
masterfrom
feat/kit-demand-signal
Sep 30, 2026
Merged

LamaSu merged 5 commits into
masterfrom
feat/kit-demand-signal

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds the kit-demand signal and the pure layer a publisher would use to release public opportunity aggregates. The signal is the private demand input that Capability Kit work uses to decide which kits to fund.

This is interface and library code only: no routes, no gateway changes, and nothing public is emitted.

  • @pcc/spec
    • UnmetReason, UnmetCapability, and a server-owned DemandEnvelope.unmet. UnmetCapabilitySchema ties the key form to the reason: no_capability_type only as a kebab slug, every other reason only as a CSD URI.
    • CallerDemandEnvelopeSchema is the caller input without any server-only field. ServerCapturedDemandEnvelopeSchema is the only schema that carries unmet. stripServerOnlyDemandFields() stays as a second guard.
    • KitDemandSignal is a private record per capability key. It holds counts by evidence class, exact verified-requester counts and histograms, with consistency refinements and a sha256:<hex> digest.
    • The public release layer, reworked after the round-1 cross-family review (pack 10, findings F1-F5):
      • one frozen PUBLIC_RELEASE_POLICY: k = 5 distinct verified requesters at or above an authenticated-order floor, with a 24 h grace. No caller can choose a policy;
      • only canonical UTC calendar-month periods, released after the month closes. Any other window, an open month, or a signal computed before the close throws;
      • only IDs in a publisher-supplied approved set that pass isPublishableCapabilityId: a CSD URI whose slug has at most 40 characters, at most 3 hyphens and no run of 4+ digits;
      • a period label, never an activity date. Counts are banded 5-9 / 10-24 / 25-99 / 100+;
      • buildPublicRelease() returns one deterministic release per period, with the approved set's digest and a release digest, for a publisher to record once.
  • @pcc/demand-intel: UnmetDemandLens folds server-captured unmet intents into validated signals. computeForPeriod() produces the input for a release.
    • It reads only first-party capture event types, never intent.external_ingest.
    • It resolves keys reason-first and can drop CSD URIs outside a registered set.
    • Verified breadth counts only actorType: "authenticated_operator" rows. Counting is exact, and timestamps come from the server row.

Not in this PR (by design)

  • Nothing is public. There is no publisher and no endpoint. The publisher (a no-parameter read surface, a server release job, a write-once ledger per period, the approved set) is separate work and defaults OFF.
  • Cross-release and cross-key privacy accounting is an open decision. A public feed waits for it and for proven requester identity.
  • The gateway half (server-computed unmet at capture, actor labels, ingest parsing) is feat(gateway): server-side unmet-demand capture, flag default OFF (R44 D2, stacked on #365) #387, stacked on this PR.
  • No data. Every fixture is synthetic.

Tests (base ac86a404)

Package Typecheck Tests
@pcc/spec clean 865/865 across 39 files
@pcc/demand-intel clean 55/55 across 5 files

The adversarial cases the round-1 review asked for:

  • two k values;
  • sliding windows at millisecond precision;
  • a CSD-shaped no_capability_type key;
  • one unverified late intent.

Also covered: open and in-grace months, signals computed before the close, exact approved-set membership, ID hygiene, release determinism and input immutability.

Each guard was mutation-checked locally: removing it fails the suite. CI build-and-test passes at 6377dd5d.

Review

  • Round 1 (pack 10): DO-NOT-SHIP as a public feed; the private signal and lens could continue. This head addresses all five findings in the pure layer. Round 2 is queued.
  • Merge is the operator's call.

🤖 Generated with Claude Code

LamaSu and others added 3 commits September 24, 2026 09:52
… (R44)

Interface work for ledger row R44 (pcc-painpoints -> pcc-kits), additive only:

- demand.ts: UnmetReason / UnmetCapability and the server-owned
  DemandEnvelope.unmet field. The caller-input DemandEnvelopeSchema
  (used by /api/intent/ingest) is unchanged, so zod still strips a
  caller-supplied unmet list; ServerCapturedDemandEnvelopeSchema is the
  only schema that carries it. stripServerOnlyDemandFields() removes
  fulfillmentPath and unmet from any envelope that arrived from outside.
- kit-demand.ts: KitDemandSignal (private; per capability key; internal
  unmet counts by evidence class plus a pointer to a private prior), with
  consistency refinements and a canonical 0x-sha256 digest; and
  toPublicOpportunityAggregate(), the only demand shape allowed to leave
  the server: allow-listed fields, banded counts, suppressed below k
  distinct verified requesters (k >= 5), never prior-only or proposed types.

Tests: 29 new (synthetic fixtures only), including forged breadth,
prior-only and proposed-type suppression, histogram consistency, strict
keys, ingest-schema non-widening and input immutability. @pcc/spec
826/826 (baseline 797), tsc clean; consumers @pcc/demand-intel 23/23 and
@pcc/intent-broker 13/13, tsc clean against the rebuilt dist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBSkBhKBWPA9AeCJ7J5G7a
…als (R44 D3)

UnmetDemandLens folds server-captured UNMET intents into one validated
KitDemandSignal per capability key, merged with optional private priors.
It is fail-closed against forged demand:

- reads only first-party capture event types; intent.external_ingest
  (caller-asserted envelopes) is never read
- counts an intent only when the server marked it unmet
  (ServerCapturedDemandEnvelopeSchema, fulfillmentPath "unfulfilled",
  non-empty unmet)
- verified breadth counts only rows the server stamped
  actorType "authenticated_operator". On master ac86a40 every capture
  point (requests, negotiation, A2A, nl-query) takes its actor from the
  request body, so breadth is 0 and nothing clears the public k until the
  gateway records the authenticated principal (D2 contract)
- firstSeen/lastSeen use the row's server timestamp, not the envelope's
  createdAt; exact Set counting, never HyperLogLog, near the k threshold
- known types must arrive as CSD URIs; only no_capability_type slugs
  become proposed:<slug>

Tests: 14 new (synthetic), including external-ingest exclusion,
body-actor zero breadth, one-principal dedupe, window filtering,
determinism across insertion order, and the seam into
toPublicOpportunityAggregate (4 principals private, 5 public; volume
alone never publishes). @pcc/demand-intel 41/41 across 5 files, tsc clean.

Correction to 58c720a's message: it said "@pcc/demand-intel 23/23".
That run had not loaded aggregator.test.ts (4 tests) because @pcc/store
was not built in the worktree. With it built, the 27 pre-existing tests
all pass against the spec change; @pcc/intent-broker 13/13 was correct.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBSkBhKBWPA9AeCJ7J5G7a
…class counts (kits review #2506)

Addresses pcc-kits' shape review of feat/kit-demand-signal (bus #2506):

- KitDemandInternal gains distinctVerifiedRequestersByClass and the exact,
  cumulative distinctVerifiedRequestersAtOrAbove (strongest class per
  principal), plus urgency, requested-assurance-tier and country
  histograms (ISO alpha-2 or "unknown"; sub-country detail is dropped) and
  the window bounds. Schema refinements enforce every sum, the per-class
  bounds, monotonicity and the union bounds.
- OpportunityAggregatePolicy gains minEvidenceClass (default and floor
  authenticated_order) and minWindowDays (default and floor 30). The
  projection counts only requesters whose strongest intent meets the floor,
  refuses any weaker policy, and reports countedEvidence. Bands follow the
  kits proposal: 5-9, 10-24, 25-99, 100+.
- kitDemandSignalDigest now returns sha256:<hex>, byte-identical to the
  async util/canonical sha256(canonicalize(x)) (a test asserts this).
- UnmetDemandLens emits the new fields; normalizeCountry is exported.

Caveat stated in code: k-anonymity is a privacy floor, not a Sybil control.
Until R28/R29 bind keys to identity, only funded evidence costs a forger
money, and no capture point emits funded evidence yet.

Tests: @pcc/spec 842/842 across 39 files (kit-demand 45), tsc clean;
@pcc/demand-intel 46/46 across 5 files (unmet-lens 19), tsc clean;
@pcc/intent-broker 13/13, tsc clean. All against the rebuilt spec dist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBSkBhKBWPA9AeCJ7J5G7a
…ound-1 F1-F5)

The round-1 cross-family review of #365 @e5fa22ba (pack 10) returned
DO-NOT-SHIP as a public feed. This commit closes the pure-layer half of
each finding. The publisher (no-parameter read surface, write-once
release ledger, approved set) belongs to pcc-kits (#3405).

F1  toPublicOpportunityAggregate has no policy parameter. One frozen
    PUBLIC_RELEASE_POLICY: k = 5, floor authenticated_order, 24 h grace.
F2  Only canonical UTC calendar-month periods ("YYYY-MM"), released only
    after close + grace. Any other window, an open period, or a signal
    computed before its period closed throws. buildPublicRelease()
    returns one deterministic release with a sha256 digest. The lens
    gains computeForPeriod().
F3  UnmetCapabilitySchema requires the key form to match the reason:
    no_capability_type only as a slug, every other reason only as a CSD
    URI. resolveCapabilityKey checks the reason first and can take a
    registered set. The projection emits only exact members of the
    publisher's approved set that pass isPublishableCapabilityId (slug
    at most 40 chars, at most 3 hyphens, no run of 4+ digits). The
    release records the approved set's digest.
F4  asOf is gone. The aggregate (now v1) carries the period label.
F5  CallerDemandEnvelopeSchema (no fulfillmentPath, no unmet) for caller
    input. The ingest route switches to it in #387.

Tests cover the verdict's adversarial cases: two k values, sliding
windows at millisecond precision, the CSD-shaped no_capability_type
counterexample, and one unverified late intent. Each guard was also
mutation-checked locally: removing it fails the suite.
spec 865/865, demand-intel 55/55.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 28, 2026
…t parses the caller schema (PX-13 round-1 F3, F5)

Follows the merge of feat/kit-demand-signal @6377dd5d (the #365
round-1 fixes).

F3  computeUnmet lists an entry only if it passes UnmetCapabilitySchema:
    a registered type only by its canonical CSD URI, a type with no CSD
    only as no_capability_type with a slug. An unmet type that cannot be
    keyed that way is counted as unkeyed, not listed: no capacity or no
    tier for an uncatalogued type, a registered URL that is not a
    canonical CSD URI, or a name that does not slugify. Such an intent
    is still marked "unfulfilled", never "auto".
F5  POST /api/intents/ingest parses with CallerDemandEnvelopeSchema,
    which has no fulfillmentPath, unmet or unmetTruncated. The strip
    stays as a second guard. The lens header now describes D2's two
    actor strengths.

The seam tests now run capture -> lens (computeForPeriod) ->
buildPublicRelease. A type publishes only at 5 proven principals, for a
closed period, and only when the publisher approved its CSD. Key
holders and proposed types never publish.
spec 865/865, demand-intel 55/55, gateway 3026 passed / 6 skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LamaSu LamaSu changed the title feat(spec,demand-intel): kit-demand signal and k-anonymous public projection (R44) feat(spec,demand-intel): kit-demand signal and fixed periodic public release layer (R44, PX-13) Sep 28, 2026
LamaSu added a commit that referenced this pull request Sep 29, 2026
The consumers acked this on the bus: readmodels (#3623), adk (#3785) and
refvertical (#3571, #3792).

- A2: OpportunityDTO.capabilityContractDigest pins the exact CSD revision.
  It is REQUIRED on a funded funded_offer, because the accepted plan pins
  it; optional on kit_build_request; forbidden on demand_aggregate.
- A3: evidence.requiredEventClasses: string[] becomes
  evidence.requiredPrimitives: CsdEvidencePrimitiveRef[], the same grammar
  as a CSD's evidence refs and setup's EmitterDecl emits[]. Each id must be
  active in EVIDENCE_PRIMITIVES, so the ADK's provenance planner compares
  supply against demand with no mapping table.
- A4: the manifest header states two conventions. The provenance-recipe
  mediaType is application/vnd.pcc.provenance-recipe+json;v=1, and
  compatibility.interfaces uses the kernel's AdapterType names, never
  "mock". There is no shape change.
- A5: a demand_aggregate carries releasePeriod (YYYY-MM), required there
  and forbidden elsewhere. asOf stays the READ time for every kind
  (readmodels' counter), so it never says when an intent happened (#365 F4).
- A6: artifact names must be safe relative POSIX paths. Before, the schema
  accepted '../../etc/passwd', a path traversal for any installer that
  writes artifacts by name. Refvertical's real R46 kit already complies,
  so its kitDigest is unchanged.
- One CSD_CAPABILITY_URL_PATTERN is now shared by the kit manifest and
  OpportunityDTO.

Golden kit digests are unchanged (A6 only validates). kits-contracts
passes 26/26 (5 new). With the source changes reverted, 8 fail. The full
spec suite passes 823/823 and tsc is clean.

A1, the OperatorBindingDTO part, follows separately: operator-ux, its last
acker, is paused (steward #4029).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT8WYWkfig4KNcxtMqvMuj
… round-2 F3)

The round-2 cross-family review (pack 38) closed F1, F2 and F4 and asked
for one F3 fix before merge. Projection checked membership with the
caller's approvedCapabilityTypes.has(), while approvedSetDigest iterated
the same object separately. So a Set whose has() or iterator disagreed
with itself could publish an ID the digest never committed.

It was reproduced first, at 6377dd5, with a throwaway test: an approved
Set with has = () => true published pcc://capabilities/secret-us-ca-sf/v1,
while the digest committed only the one iterated ID.

Now the approved identifiers are read once: only strings that pass
isPublishableCapabilityId, deduplicated and sorted, held in a fresh native
Set. That one snapshot drives both membership and approvedSetDigest, in
toPublicOpportunityAggregate and in buildPublicRelease.

Tests: a lying has(), an iterator that changes between passes (read
exactly once), and a mixed set (only publishable strings are committed).
Mutation-checked: all 3 reversions fail. spec 868/868, demand-intel
55/55.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LamaSu
LamaSu marked this pull request as ready for review September 30, 2026 19:18
@LamaSu
LamaSu merged commit 75fd440 into master Sep 30, 2026
5 checks passed
LamaSu added a commit that referenced this pull request Oct 1, 2026
…ease (astra 112b A)

Slug syntax is not a privacy boundary ("alice-smith" passes
isPublicCapabilityUrl), so a demand_aggregate now has to name a capability in
an approved public set, and has to name the release record it came from.

- BUILTIN_PUBLIC_CAPABILITY_URLS: the CSD urls compiled into @pcc/spec
  (loadBuiltinCsds), filtered, sorted by code unit, frozen. That is 8 urls:
  document-print-and-mail/v1 is a draft workflow CSD that loadBuiltinCsds does
  not register and the package build does not ship, so it is not in the set.
- publicCapabilityUrls(activeKitCsdUrls): built-ins plus ACTIVE kits' csd urls;
  throws on any entry that is not a public capability url.
- approvedSetDigest(urls): same construction as #365's approvedSetDigest, pinned
  by a golden vector (also checked once against #365's real buildPublicRelease).
- DemandAggregateDTO.releaseDigest is required.
- opportunityDTOSchemaFor(urls) snapshots the set once; demand_aggregate needs
  membership. OpportunityDTOSchema is built from the built-ins.
- demandAggregatesFromRelease(release, approvedUrls, asOf) rebuilds the DTOs and
  throws unless the record's digest, approved-set digest, periods and
  membership all hold.
- isPublicCapabilityUrl is documented as a syntax backstop, not a boundary.

The aggregate() fixture moves to a built-in capability, period 2026-08 and past
instants; the opportunity shape fingerprint is re-pinned (releaseDigest).

Agent: implementer-kilo

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT8WYWkfig4KNcxtMqvMuj
LamaSu added a commit that referenced this pull request Oct 3, 2026
…he release contract)

Master carries painpoints' #365 (kit-demand.ts: PUBLIC_RELEASE_POLICY,
isReleasePeriodClosed and PublicOpportunityRelease), which astra 112c asks
demandAggregatesFromRelease to reuse. It also carries 2d80818 (board N64),
which registers document-print-and-mail/v1 in loadBuiltinCsds.

The one conflict-free consequence is planned: the built-in public set
follows the registry, so its pinned list gains that url, in sorted
position, here (the test said to do it in the merge commit).

Spec 1181/1181 (48 files); package tsc and the contract test file's tsc
exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu added a commit that referenced this pull request Oct 3, 2026
…e executability, module digests

All three open findings were reproduced at a173743 first (scratchpad
repro-112c: three failing tests plus a passing control, and a mutation
trace for MEDIUM 8).

CRITICAL 1 (demand privacy). demandAggregatesFromRelease now enforces
#365's exact release contract, reused from types/kit-demand.ts (on this
branch since the master merge):
- the record's policy must be PUBLIC_RELEASE_POLICY's k and evidence
  floor exactly, and every aggregate must be counted at that floor
  (strict literals; the verdict's k 0 / query record is refused);
- the period must be releasable under #365's rule, isReleasePeriodClosed:
  ended at least the 24-hour grace ago by this clock;
- PublicOpportunityReleaseRecord is now #365's PublicOpportunityRelease.
A demand_aggregate DTO's asOf must also fall after the period's close
plus that grace, since no release can exist earlier.

HIGH 4 (executability). evidenceIsExecutable checks the CUMULATIVE
program, tier 0 through the target. Each tier gets the refs that support
it and whose dependencies are satisfiable there (fixpoint), so it is the
best assignment the refs allow. computeCsdEligibility's eligibleTier
must then reach the target, with live verifiers. A ref that can
contribute at no tier up to the target fails it. Payer approval alone is
no longer executable at tier 2 or 3, because its tier 1 set is empty
(the verdict's reproduction is a test). Positive tier 1-3 cases mark the
stub ident.registered_key live inside a try/finally, because no tier
1-3 set is executable with today's registry.

MEDIUM 8 (versioning). The lock table also pins the digest of each
literal's own module, so any edit there fails CI until it is
re-pinned. That includes a refinement whose effect lies outside the
finite corpus (the verdict's reproduction now fails on exactly that
pin). The Versioning docs list the inputs that live outside each module.

Corpus: the payer-only accept cases become honest non-executable cases.
New rejects cover payer-only executable claims and reads inside the
grace; the demand accept cases move past the grace. The generator is
deterministic: the binding and kit corpora came out byte-identical.

Tests: spec 1185/1185 (48 files); package tsc and the contract
test-file tsc exit 0. Mutations: 8 of 8 killed (grace check, policy,
counted evidence, DTO grace, single-tier executability, non-contributing
ref, dependency fixpoint, and the out-of-corpus refinement).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu added a commit that referenced this pull request Oct 3, 2026
…irement

CRITICAL 1 (reproduced at cbc1c14): demandAggregatesFromRelease accepted a
self-consistent release whose aggregates were in reverse code-unit order,
a record #365's buildPublicRelease can never write. The reader now requires
capabilityType strictly increasing in code-unit order (the order #365 sorts
into), so the order carries no information.

HIGH 4 (reproduced): the dependency pass silently removed a named primitive
and then judged the reduced program, so confirm.execution_mode alone was
executable at tier 0, and registered_key + execution_mode (no receipt) at
tier 1 with the key live. Every named ref must now sit in some tier;
otherwise the requirement is not executable. That rule subsumes the old
"supports some tier" pre-check, which is removed. The tier-1 expectation
that pinned the hole is now false.

LOW: the header names every external input (csd/schema.ts, capability-kit.ts).

Corpus: 3 new cases (97). LOCK re-pinned: opportunity corpus 4fb26628cccab46e,
source 4deb00fd1332e7d0; shape unchanged. Spec 1187/1187, both tsc 0,
mutations 11/11 killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu added a commit that referenced this pull request Oct 3, 2026
feat(gateway): server-side unmet-demand capture, flag default OFF (R44 D2, stacked on #365)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant