Factory docs, home
Page navigation

Reference document, shown as written except that local paths appear as placeholders. Where it describes the Factory as intended, read it as design, not current state: only local infrastructure is accepted, no release, customer value or scheduled automation is established, and all 27 customer-value epics remain open. Current state: Factory model.

The Operations-owned registry of local Products, plus read-only views of sourced facts about them. Source: src/portfolio.ts (registry, fact streams, views) and src/portfolio-inventory.ts (trusted read-only collectors). It depends only on the accepted journal and canonical-JSON contracts. It does not import the repository-sealing module.

Not implemented here: HTTP, the dashboard, repository sealing/CAS, provider workers, timers, background refresh, product delivery, and admission of delivery Authority. There is no network access, provider invocation, installation, CI or account access. Nothing in a user's Product is mutated.

Boundary

  • Registration binds a Product ID to a verified primary Git checkout. It grants metadata inspection only (operationScope: "metadata-inspection"). Views currently report deliveryAuthority: "absent" because no delivery Authority domain is composed here. Registration does not establish whether authority exists elsewhere; future composition must read the authoritative Product gate.
  • A Product ID is never removed and never re-bound. Branch, HEAD and display-name changes do not affect it.
  • Views dispatch nothing, run no Git and execute no command. They work over CommandJournal.openReadOnly. SQLite may still touch its -wal/-shm coordination files.
  • History comes from readHistory. The pending outbox is not complete history and is never read.

Journal layout

AggregateHoldsBound
portfolio:localRegistry: Product records, sorted by ID100 Products
portfolio:product:<id>Fact stream for one Product. State is an index of records (revision, digest, kind, Product); history is every accepted observation.2,000 records, 10,000 events
portfolio:usageUsage stream shared by all Products, plus a session-attribution index2,000 records, 500 sessions, 10,000 events

The worst-case stream index is about 688 KB, within the journal's 1 MiB document bound. A stream at capacity refuses new records with LIMIT. It never truncates. Long-running use will need a segmentation design first.

API

const portfolio = new Portfolio(journal, { git?: { gitPath?, timeoutMs? }, now?: () => Date });

portfolio.listProducts(): Registry                                     // { version, products }
await portfolio.registerProduct({ commandId, expectedVersion, productId, displayName, root }): RegistrationResult
portfolio.renameProduct({ commandId, expectedVersion, productId, displayName }): RegistrationResult
await portfolio.inspectProduct(productId): InspectionResult            // explicit command; records a repository fact
portfolio.recordObservations(productId, observations): ImportResult    // 1-100 product facts, atomic
portfolio.recordUsage(receipts): ImportResult                          // 1-50 usage-receipt/1
portfolio.recordUsageObservations(observations): ImportResult          // usage coverage and retractions
portfolio.productView(productId, { window, asOf, staleAfterMs? }): ProductView
portfolio.portfolioView({ window, asOf, staleAfterMs?, after?, limit? }): PortfolioView   // limit 1-100, default 20
portfolio.currentConditions({ window, asOf, staleAfterMs? }): ConditionsView           // repository, blockers, source notices; each stream isolated
portfolio.blockerRecord(productId, blockerId, { asOf, staleAfterMs? }): BlockerRecordView // governing report and newest 50 revisions

inspectRepository(root, { gitPath?, timeoutMs? }): Promise<RepositoryInspection>        // collector
claudeResultReceipt(resultObject, { capturedAt, observedAt, attribution }): UsageReceipt   // pure mapping

Errors. PortfolioError.code is one of:

  • INVALID: malformed input, refused before anything runs or is written.
  • CONFLICT: contradicts what is already recorded.
  • NOT_FOUND: no such Product.
  • LIMIT: a bound would be exceeded.
  • UNAVAILABLE: the root cannot be registered as it is.
  • CORRUPT: a stored record fails re-validation.

Journal errors such as VersionConflictError, CommandConflictError and JournalReadOnlyError pass through unchanged. Receipt errors never quote input values or unsupported field names.

Registration and identity

registerProduct runs the collector itself; the caller cannot supply an identity. The collector's result is RepositoryIdentity:

  • canonical root;
  • commonDir equal to <root>/.git;
  • device and inode numbers of both directories;
  • object format.

The identity has no HEAD, branch, dirty state, worktrees or remotes. bindingDigest is the SHA-256 of that canonical identity.

  • Idempotence. A replayed command ID returns its stored result (replayed: true). Re-registering exactly the same binding under a new command ID, even with a stale expectedVersion, returns unchanged and writes nothing.

  • Conflicts. Registration is refused (CONFLICT) when:

    • the ID is already bound to another repository;
    • the same ID is registered with a different name (use renameProduct instead);
    • the repository is already bound to another ID. This is detected by root, common directory, or root/common-directory device+inode, so it covers other paths to the same directories.
  • Refused roots. Symbolic-link aliases, non-canonical paths, linked worktrees, submodules and gitlinks, symbolic-link .git, a redirected core.worktree, and bare repositories are refused with UNAVAILABLE.

  • Inspection results. inspectProduct compares a fresh inspection with the binding and records one of:

    • available;
    • missing;
    • replaced (different device/inode/format, or the path is now a link or a non-directory);
    • not-repository;
    • not-primary;
    • unreadable.

    Nothing is created, repaired or re-bound. HEAD is recorded only when the result is available.

Collector process boundary

  • Invocation. A fixed executable (default /usr/bin/git) with fixed arguments: --no-pager --no-replace-objects --no-optional-locks, core.fsmonitor=false, core.hooksPath=/dev/null, core.untrackedCache=false and protocol.allow=never. No shell and no stdin.

  • Environment. Only these variables are passed:

    • PATH=/usr/bin:/bin, LC_ALL=C;
    • GIT_CONFIG_NOSYSTEM=1, GIT_CONFIG_GLOBAL=/dev/null, GIT_ATTR_NOSYSTEM=1;
    • GIT_OPTIONAL_LOCKS=0, GIT_TERMINAL_PROMPT=0, GIT_NO_REPLACE_OBJECTS=1;
    • GIT_NO_LAZY_FETCH=1, GIT_ALLOW_PROTOCOL= (empty);
    • GIT_CEILING_DIRECTORIES=<parent>.

    Inherited GIT_DIR, GIT_WORK_TREE and GIT_CONFIG_PARAMETERS cannot redirect inspection.

  • Commands. Only three: rev-parse (layout and format), symbolic-ref --quiet HEAD, and rev-parse --verify --quiet HEAD^{commit}. No status, diff, fetch or worktree command runs, so no index refresh, clean/smudge filter, credential helper or promisor/remote helper is reached.

  • Bounds. 5 s timeout by default (100 ms-30 s allowed), then SIGKILL. Stdout is capped at 64 KiB and must be UTF-8; stderr is capped at 8 KiB and discarded. If device or inode numbers change during inspection, the result is unreadable.

  • What is never read. Source files, remote URLs and credentials. Dirty state is not collected.

Observations

{ collector, recordId, revision, productId, sliceId, observedAt, capturedAt, fact }
  • observedAt is when the fact happened according to its source. It is null when unknown and is never guessed from filenames or mtimes.
  • capturedAt is when the collector read the fact.
  • Instants are UTC and normalised to milliseconds. observedAt cannot be after capturedAt, and a declared window cannot end after its capture.
  • Text is 1-280 printable characters. Control characters, line separators, bidi overrides, BOM and outer whitespace are refused.
  • An observation is at most 8,192 UTF-8 bytes. Input must be plain JSON: accessors, prototypes, holes and reserved keys are refused.

Record identity. In a product stream a record is (collector, recordId). In the usage stream it is recordId alone (anthropic/invocation/<id>, anthropic/session-snapshot/<id> or coverage/<name>), so two collectors reporting the same invocation produce one record.

Import rules. The whole batch is atomic.

IncomingResult
New recordrecorded
Same revision, same content digest (capturedAt excluded; collector also excluded for usage)duplicate, nothing written
Same revision, different contentCONFLICT, whole batch refused
Lower revisionobsolete, ignored
Higher revision, same kind and Productcorrected: supersedes; history keeps both
Higher revision of kind retractionretracted: the record no longer asserts anything

Changing a record's kind or its Product is CONFLICT. Command IDs are derived from (stream, version, payload), so concurrent or retried identical imports replay rather than record twice. productId must name the stream's own Product; cross-Product mixing is refused. local-git repository facts come only from inspectProduct.

Supported facts

KindFieldsSemantics
activityentityType, entityId, transitionwork: created, integrated, integration-unknown, reverted. proposal: rejected. pull-request: opened, changes-requested, closed-unmerged, merged. issue: opened, closed. Each is counted separately.
verificationrunId, status, currentValidityFor verified, failed or inconclusive (historical Verdicts), validity is valid, invalidated or unknown. For cancelled, cancellation-pending, not-completed or unresolved (not settled, effect unknown), validity is not-applicable.
releasereleaseId, channel (alpha, stable), transition (available, withdrawn)Availability, kept apart from merges and verification
measurementmetric, value, unit, windowA measured value for its own window; never summed
focusobjective or null; the envelope's sliceIdExplicitly stated focus only. Null is an explicit "no focus".
blockerblockerId, state, prerequisite, affected[], remediesTried[], nextAction, optional detailsOpen and resolved states. details is { responsible (owner, factory, external or null), exhaustion, steps[], remedyEvidence[], resolutionEvidence[] }, at most 10 items per list; resolution evidence only on a resolved report. A fact without details keeps its original shape and digest, and reads as details: null (unknown).
source-coveragebasis, window, statusBasis is work, proposal, pull-request, issue, verification, release or usage. Status is complete, partial, unavailable, unauthenticated or corrupt.
repositoryavailability, headRef, headOidOnly from inspectProduct
usage-invocation, usage-sessionSee UsageUsage stream only, created from receipts
retractionnoneWithdraws a record

Native journal facts. A trusted collector may import factory Runs as verification facts with, for example, collector factory-journal, recordId execution:<runId>, and revision set to the source aggregate's version. That makes re-imports duplicates and newer versions corrections. The collector must read readHistory or readAggregate, never the outbox. No such collector ships in this module.

Views

Counts. A Count counts distinct entity IDs that made one transition within a half-open UTC window [start, end). It takes one of three states:

  • exact requires all of:

    • complete coverage for the count's basis spans the window (union over records and collectors);
    • no overlapping record reports partial, unavailable, unauthenticated or corrupt;
    • no relevant undated record.

    Zero is only ever exact.

  • partial gives atLeast together with reasons.

  • unknown means nothing establishes a value.

The possible reasons are:

  • no-complete-coverage, coverage-gap, coverage-ends-before-window-end;
  • source-partial, source-unavailable, source-unauthenticated, source-corrupt;
  • undated-records, unknown-values;
  • recorded-sessions-only, unresolved-session.

A problem declaration stops blocking exactness only when its source corrects that same record to a higher revision. Undated records never enter dated totals.

Current-state facts are focus, repository, blockers, per-Run verification and per-(metric, window) measurement. For each subject, the most recently captured one stands; later journal order breaks ties. Focus and repository use Known<T> with source, observation/capture times and a stale flag (default 24 h); absent facts are unknown. Blockers retain source, capture time and staleness; measurements retain source, capture time and their own window. Verification is projected into counts with explicit validity distinctions. No blocker facts means "none reported", not "none exist". Measurements alone do not establish a Supported customer Outcome; this module has no Outcome assessment contract.

asOf is a capture cutoff, not a historical snapshot:

  • facts captured after asOf are ignored;
  • each stream is replayed only up to the version its state had when read (watermark);
  • a later import may still add facts captured before asOf;
  • the registry is read as it is now.

Replay integrity. A replay re-validates every event and must reproduce the stored index exactly; otherwise CORRUPT. portfolioView reports a corrupt Product as { state: "corrupt" } and still returns the other Products.

Usage

Input. The only accepted input is usage-receipt/1, parsed with exact keys at every level:

  • provider: "anthropic", collector, invocationId, sessionId;
  • observedAt, capturedAt;
  • attribution { productId | null, sliceId | null };
  • invocation (per-invocation counts) or null;
  • session (session-cumulative snapshot) or null.

Unknown keys (prompts, completions, thinking, auth) are refused without echoing them.

Counters. There are four disjoint counters: uncached input, cache read, cache creation and output. Thinking tokens are part of output and are not stored separately. null means unknown, never zero. No grand total is ever computed.

Mapping a Claude Code result. claudeResultReceipt maps one final result object:

  • usage becomes invocation;
  • modelUsage and total_cost_usd become session, only when session_id is valid;
  • observedAt comes from the caller. The result line carries no time, so it stays null unless a trusted time is supplied.

It reads only allowlisted fields, calls no accessors and retains no completion text. It reads no file. Automatic import of raw transcripts is not implemented.

Invocations. Each is one record, whichever collector reports it, and it belongs to one Product/Slice or stays unallocated. A session's attribution is fixed by its first record. Conflicting attribution is CONFLICT. Invocations dated within the window are counted and their tokens summed. Undated invocations are counted in undated and are never allocated to a window.

Sessions. A session's snapshots resolve only when all three hold:

  • every value in every snapshot is known;
  • for any two snapshots, one has every field of the other with no smaller value (a model may appear later but may not disappear);
  • where both snapshots are dated, the later one is not smaller.

When resolved, the greatest snapshot is the session's total so far. Otherwise the session is unresolved. Snapshots are never added to each other or to invocation figures. Session totals are summed only over recorded sessions, so they are at best partial (recorded-sessions-only). Their time allocation is unknown, so window deltas are not computed.

Cost. Cost is labelled costBasis: "provider-reported-estimate". It is not billing.

Unallocated usage. Usage attributed to no Product appears only in portfolioView().unallocatedUsage.

Unknown sources

Nothing here collects from:

  • GitHub PRs and issues;
  • releases or store availability;
  • telemetry and customer outcomes;
  • billing;
  • Notion;
  • Capability health.

These stay unknown until an explicit, authorised collector records facts and coverage. Local Claude result files have no trustworthy timestamps, so their usage stays undated. Dirty state, remote identities and worktree lists are deliberately not collected.

Known limits

  • Device changes. Identity includes device numbers. If a volume is remounted with a different device number, the Product reads as replaced. This fails closed, but no re-binding command exists yet. The six commissioning repositories are on the internal data volume.
  • Firmlinks. Detection of other paths such as macOS firmlinks relies on the device+inode comparison.
  • Git shim. /usr/bin/git is Apple's xcrun shim, which may use its own cache outside any repository.
  • Timing. A second inspection within the same millisecond that reaches a different result is refused as a conflict.
  • Capacity. The limits in the journal-layout table are explicit and final for this increment.

Commissioning data (verified)

The six Products requested on 2026-09-28 are commissioning data, not application constraints. After independent acceptance, all six were registered and inspected beneath <home directory path>. Registry version 6 and a read-only reopen were verified; HEAD, config and index bytes were unchanged. See commissioning receipt.

Product IDRoot directory
justspeaktoitjustspeaktoit
pastapasta
family-watchfamily-watch-activity-tracker
tallytally
focus-sentinelfocus
software-factorysoftware-factory

Registration and inspection read metadata only. They preserve dirty checkouts and grant no UI-test, model, repository-mutation, release, account or spending authority. Focus Sentinel's documented UI-test restriction is unaffected.

Tests

Tests use disposable, test-owned repositories and journals only.

FileCovers
test/portfolio-inventory.test.tsIdentity and unchanged bytes; unborn and detached HEAD; malformed roots; missing, alias, nested, worktree, gitlink, core.worktree and bare cases; hostile hooks, fsmonitor, filters, helpers and inherited variables; promisor lazy fetch; timeout, flooding and lying Git; Claude result mapping
test/portfolio.test.tsReplay, restart and identical re-registration; alias and worktree duplicates; conflicts; rename; missing, replaced and linked roots never recreated; device change; the 100-Product bound; concurrent processes
test/portfolio-observations.test.tsDeduplication across collectors; corrections, retractions, obsolete revisions and conflicts; exact zero versus unknown or partial; stale data; distinct verification outcomes; explicit focus; hostile or oversized input; stream bound; read-only views with no writes or Git; stored corruption
test/portfolio-usage.test.tsCumulative replay and reordering; unresolved sessions; window allocation and coverage; exclusive attribution; strict receipts

Source: docs/portfolio.md