@forgrit/shared

中文文档
0.3.0发布于 2026/09/26MITTypeScriptESM + CJS零依赖近 30 天下载 2847

Cross-pillar shared infrastructure for ForGrit's three repos (Software Foundry, Software Orchestrator, Workforce): a hash-chained evidence ledger and resilience primitives (retry, circuit breaker, rate limiter).

原始使用文档
文档目录15 节
  1. What does NOT belong here
  2. Purpose
  3. Subpath Exports
  4. @forgrit/shared/design-system
  5. @forgrit/shared/engineering-contract
  6. @forgrit/shared/evidence
  7. @forgrit/shared/resilience
  8. Installation
  9. Development
  10. Verification
  11. CI/CD
  12. Release policy
  13. Sibling projects
  14. Usage
  15. License

@forgrit/shared

Cross-pillar contracts and resilience primitives used by Software Foundry, Software Orchestrator, and Workforce.

Status: Active internal package (0.3.0 in the manifest, not yet published — npm latest is 0.2.0) · Runtime: Node.js 20+ · Owner: @forgrit-legacy · CI: default-branch pipeline

What does NOT belong here

This package is scoped, cross-repo infrastructure only — not a dumping ground. Explicitly excluded (see docs/superpowers/plans/2026-08-17-software-foundry-rename-and-shared-package.md's Global Constraints for the full reasoning):

  • Software Orchestrator's actual orchestration intelligence (mission-control, kernel, planner, execution, brain, repository-* modules)
  • Software Foundry's product engine (blueprint, codegen-types, debugger, design-*, shared/contracts-prompt)
  • Workforce's employee-governance model (Principal, RoleCard, authorize())
  • The Repository Intelligence "Frozen Five" contract (Snapshot/Finding/Metric/Relationship/Insight) — gated behind its own stage-promotion rule, not ready yet
  • software-orchestrator/modules/orchestrator/connector-runtime/RetryPolicy.ts or RateLimiter.ts as source material to copy from (both have known compile-error bugs; this package's resilience module uses their design ideas, not their code)

Purpose

This package is not a dumping ground for utility code. It hosts only infrastructure types and primitives that:

  • Are consumed by more than one sibling repo (Foundry, Orchestrator, or Workforce)
  • Carry cross-repo invariants (e.g., evidence ledger record structure)
  • Do not belong in any single pillar's codebase

All new exports require evidence that the need appears in at least two consumers.

Subpath Exports

@forgrit/shared/design-system

The ForGrit design-token pipeline: immutable pulled revisions of the Claude Design kit, a role contract that names what those tokens mean, per-consumer bindings, emitters for the artefacts each consumer needs, and the gates a revision must pass before it can be adopted.

What the subpath ships

ImportWhat it is
@forgrit/shared/design-system/css/tokens.cssPlain CSS: :root and .dark custom properties, framework-free.
@forgrit/shared/design-system/css/tailwind4.cssThe same tokens plus a Tailwind 4 @theme inline block aliasing them.
@forgrit/shared/design-system/tailwind3-presetA Tailwind 3 preset — theme.extend, whose values are var(--token) references.
@forgrit/shared/design-system/tokens.jsonEvery emitted token as data, with a $generated provenance string.
@forgrit/shared/design-system/tokensThe same values as one typed TypeScript constant, DS.
@forgrit/shared/design-system/contractThe 74-role contract — ROLES, ROLE_IDS, getRole, assertKnownRole.
@forgrit/shared/design-system/adapters/contractadaptTokenSystem — a Design Director's token spec projected onto the role contract.
@forgrit/shared/design-systemDESIGN_SYSTEM_SCHEMA_VERSION and the shared types.
forgrit-ds (bin)The CLI: pull --seal, diff, emit, sync, check, report, assert-publishable.

Every generated file carries the same first-line header — @generated by @forgrit/shared/design-system <version> from <family>/<revision> sha256:<contentHash> — in a CSS comment, a TypeScript comment or a JSON $generated field, so any artefact in any repository names the revision it came from.

The revision model

A revision is a directory that is written once and never rewritten: src/design-system/revisions/<family>/r<NNN>/, holding the pulled bytes (colors_and_type.css, _ds_manifest.json) and a revision.json whose contentHash is a SHA-256 over the canonical JSON of the sorted [{path, sha256}] file manifest — the design-corpus rule, so a revision hashes identically here and there. CURRENT is a one-line file naming the revision the package generates from, and design-sync.jsonl is an append-only log of the round trip a revision makes — push, pull, validate, reject, seal. pull --seal records the pull (from the pull's own .pull.json receipt) and the seal; a passing check run from a checkout records validate. Each step is recorded once per revision and content hash, so re-running it appends nothing on any day, and a line that disagrees about a revision's hash is refused rather than appended. Re-running pull --seal on identical bytes exits 0 and writes nothing; new bytes in a sealed ordinal are refused. Changing a sealed revision's bytes is caught by the revision-immutability gate rather than by a reviewer.

CURRENT names r001, the first revision pulled from Claude Design: provenance design-sync, parent r000, sealed on 2026-09-25 by the DesignSync runbook, which also records how the pull was done. Its stylesheet still carries the kit's own lineage comment ("Extracted from web/src/app/globals.css and layout.tsx"); the provenance is about where these bytes were pulled from, not where the kit's author first took the values. r000 stays in the tree, byte-for-byte unchanged, as r001's parent. It is provisional: its provenance is local-copy, meaning the bytes were copied in by hand rather than pulled. forgrit-ds assert-publishable (which prepublishOnly runs) refuses to publish while CURRENT names a local-copy revision, and emit, sync and check refuse one too unless --allow-local-copy is passed. report deliberately does not — describing a tree's drift is what a drift report is for, whatever the revision's provenance.

Revision bytes are public by design. A sealed revision ships inside the published package, so everything in one is world-readable the moment the package is published. Design tokens are the intended payload; nothing else belongs in a revision directory.

Consumer flow

npx forgrit-ds sync --root /path/to/consumer-repo   # write the generated files
npx forgrit-ds check --root /path/to/consumer-repo  # byte-diff them, and run all seven gates

sync writes the profile's targets into the consumer's tree — generated files that are committed to the consumer repository, so a build never depends on this package having been run. check regenerates the same targets into memory and byte-compares them with what is on disk, which is what a consumer's CI should run to prove nobody hand-edited a generated token. Neither command writes anything outside the target list.

Optional binding rows are emitted only when the revision authors them. --ember-strong and the --ember-fill* triad are bound as optional because r000 never authored them. r001 does — --ember-strong #B84C00 (#FF6A00 in .dark), --ember-fill #B84C00, --ember-fill-hover #C25000, --ember-fill-active #A34400 — so the package now emits all four with the kit's own values. The rule still decides what happens for a revision that omits them: after role resolution they do carry a value — the contract fills them from color.brand — and emitting that fallback would publish #FF6A00 under a name the revision never wrote. That would be actively harmful: #FF6A00 measures 2.87:1 on white and fails WCAG AA at body size, which is why the kit's own --ember-fill is #B84C00. So an optional row whose value came from a contract fallback is skipped, and only a value the revision itself authored is emitted.

Drift

forgrit-ds report --root <foundryRoot> [--orchestrator-root <dir>] classifies every custom property the consumers declare against the shipped revision, in both themes. The measured result for the shipped r001 is committed at docs/design-system/drift-r001.md; the earlier measurement against r000 stays at docs/design-system/drift-r000.md.

Not covered (deferred)

Phase 0 stops at a reproducible token pipeline. It does not do any of the following, and none of them is blocked by it:

  • regenerating raster assets from a revision
  • visual baselines in CI
  • re-basing the consumers' shadcn oklch() scaffold onto kit values
  • promoting the rgb()/hsl()/oklch() value-format lint from advisory to a gate
  • shipping woff2 font binaries in the package
  • the premium theme for the ForGrit app (corpus-only today)
  • a version column on DesignCapsuleVersion / FrozenScreen
  • the apps/api preview lane
  • CI-side extraction of tokens from a running app
  • DTCG (design-tokens.json) export
  • automated publishing — releases stay manual

@forgrit/shared/engineering-contract

The versioned authority payload shared by customer control-plane and Workforce callers of Software Orchestrator. parseExecutionTicket() validates untrusted decoded input, rejects unknown fields (including raw credentials), enforces issuer/principal pairing and expiry, and returns a defensive projection.

Signature verification, nonce/idempotency persistence, repository digest recomputation, and credential brokering remain receiver responsibilities; the shared package defines the interoperable payload rather than pretending schema validation is authentication.

@forgrit/shared/evidence

A hash-chained ledger for recording system decisions and side effects. Every record stores a SHA-256 hash over its own fields plus its predecessor's hash, so editing any historical record breaks the chain from that point onward and verifyChain reports exactly where.

What this gives you, and what it does not

Be precise about the security property, because the shape of the guarantee decides where the ledger is worth deploying:

  • The hashes are unkeyed. Nothing here is signed. There is no HMAC, no key, no signature — only SHA-256 over a canonical serialization.
  • The records are not immutable. They are plain JavaScript objects. readonly appears on array parameters and constrains this package's own functions; it does not freeze anything a caller holds.
  • It detects partial edits. An actor who rewrites one stored record — or a few — and leaves the rest alone is caught: the recomputed hash no longer matches, and every subsequent prevHash link is wrong.
  • It does not stop a full rewrite. An actor who can rewrite the whole stored ledger can recompute every hash forward from the edit and produce a chain that verifies cleanly. There is nothing in an unkeyed chain that such a writer cannot reproduce.

So this is tamper-evident against narrow tampering and accidental corruption, not a signature scheme. Its real value comes from pairing it with an append-only storage layer that restricts who can rewrite history: the storage layer makes wholesale rewriting hard, and the chain makes anything less than a wholesale rewrite visible. On its own, it is a corruption detector.

Invariants

  • seq is 0-indexed. The first event in a chain is seq: 0 with prevHash: GENESIS_HASH; each later event has seq === previous.seq + 1 and prevHash === previous.hash.
  • The hash preimage is an explicit, versioned field projection — v, seq, prevHash, type, subjectId, actorId, payload, observedAt — never a spread of whatever the caller passed. Extra fields on a stored record (a row id, an ORM's own columns) are outside the chain's protection and cannot perturb a recomputed hash. HASH_VERSION is baked into the preimage so a future change to that projection is distinguishable rather than silently incompatible.
  • payload must be JSON-safe. appendEvent throws InvalidEvidenceEventError on values whose canonical form would be lossy — Date, Map, Set, RegExp, Error, class instances, NaN/Infinity, bigint. Convert first: date.toISOString(), Object.fromEntries(map), { name, message } for an error. This is deliberate: a Date used to serialize as {}, so two records with timestamps decades apart hashed identically.
import { appendEvent, verifyChain, type EvidenceEvent } from '@forgrit/shared/evidence';

let chain: EvidenceEvent[] = [];

// A null head starts a chain: seq 0, prevHash = GENESIS_HASH.
chain = [...chain, appendEvent(null, {
  type: 'mission.completed', subjectId: 'mission-1', actorId: 'employee-1',
  payload: { outcome: 'success' }, observedAt: new Date().toISOString(),
})];

// Afterwards, pass the head — just `{ seq, hash }`, not the whole history, so a
// database-backed caller appends after reading a single row. An EvidenceEvent
// satisfies that shape, so the previous event can be handed over directly.
chain = [...chain, appendEvent(chain[chain.length - 1], {
  type: 'mission.archived', subjectId: 'mission-1', actorId: 'employee-1',
  payload: { reason: 'completed' }, observedAt: new Date().toISOString(),
})];

verifyChain(chain); // { valid: true, length: 2 }

Verifying a page out of the middle of a long ledger, rather than loading all of it:

verifyChain(pageOfRows, {
  expectedFirstSeq: 1000,
  expectedFirstPrevHash: lastRowOfPreviousPage.hash,
});

With no options, verifyChain anchors at seq: 0 / GENESIS_HASH and verifies a whole chain.

@forgrit/shared/resilience

Resilience primitives for distributed systems: retry with exponential backoff, a circuit breaker, and a token-bucket rate limiter.

import { withRetry, CircuitBreaker, isTransientError } from '@forgrit/shared/resilience';

const breaker = new CircuitBreaker({ name: 'external-api', failureThreshold: 5 });

async function callApi(): Promise<Response> {
  return breaker.execute(() =>
    withRetry(() => fetch('https://api.example.com'), { maxAttempts: 3 }),
  );
}

Retry classification. withRetry retries only what isTransientError accepts: ECONNREFUSED, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN. It reads a structured error.code first and falls back to the message text, but only where a code sits in machine-written position — so new Error('user typed ETIMEDOUT into the form') is not treated as a network timeout. It also walks the cause chain (and AggregateError.errors), which is what makes the fetch example above work: Node throws TypeError: fetch failed and puts the real network error in cause, where a message-only check would never find it.

ENOMEM and ENOSPC are deliberately not retried — memory and disk exhaustion do not clear because a caller tried again, and retrying adds load to a host already out of the resource.

Supply isRetryable to classify errors yourself. It replaces the built-in check rather than extending it; call isTransientError inside your predicate to keep both:

await withRetry(fn, {
  isRetryable: (error) => isTransientError(error) || isRateLimited(error),
});

Circuit breaker probe limit. While HALF_OPEN, the breaker admits at most halfOpenMaxProbes calls at a time (default: halfOpenSuccessThreshold), and canExecute() returns false beyond that until the circuit transitions. The count is of probes in flight — admitted and not yet settled through recordSuccess/recordFailure.

Known limitation: a probe that never settles (the caller never reports its outcome — e.g. it hangs with no timeout of its own) holds its slot permanently. If enough probes hang to reach halfOpenMaxProbes, the circuit is wedged in HALF_OPEN indefinitely, with no time-based recovery — only an explicit reset() clears it. Give every probe you drive through the breaker its own timeout so it always settles.

Installation

npm install @forgrit/shared

Development

Use Node.js 20 or later. Install the locked development dependencies before making a change:

npm ci

Use a feature branch and open a merge request rather than pushing directly to main. See CONTRIBUTING.md for the contribution workflow.

Verification

Run the complete local gate before opening a merge request:

npm run build
npm test
npm pack --dry-run

CI/CD

The default-branch pipeline verifies the package build and tests. It does not publish the package.

Release policy

Publishing remains manual. A maintainer reviews the package contents with npm pack --dry-run and publishes only after the required verification succeeds.

Sibling projects

Usage

Each subpath is independently importable:

// Import from design-system subpath
import { DESIGN_SYSTEM_SCHEMA_VERSION } from '@forgrit/shared/design-system';
import { ROLES, getRole } from '@forgrit/shared/design-system/contract';
import { DS } from '@forgrit/shared/design-system/tokens';
import '@forgrit/shared/design-system/css/tokens.css';

// Import from evidence subpath
import { /* ... */ } from '@forgrit/shared/evidence';

// Import from resilience subpath
import { /* ... */ } from '@forgrit/shared/resilience';

// Import from engineering-contract subpath
import { parseExecutionTicket } from '@forgrit/shared/engineering-contract';

There is no root export: every entry point above is a subpath, so require('@forgrit/shared') fails with ERR_PACKAGE_PATH_NOT_EXPORTED by design.

License

MIT — see LICENSE.

版本与发布记录

@forgrit/shared 共发布 3 个版本,首次发布于 2026/08/17;过去 12 个月发布了 3 个版本。

版本发布日期距今
0.3.0latest2026/09/261 个月前
0.2.02026/08/192 个月前
0.1.02026/08/172 个月前

关于 @forgrit/shared 的常见问题

@forgrit/shared 是什么?

Cross-pillar shared infrastructure for ForGrit's three repos (Software Foundry, Software Orchestrator, Workforce): a hash-chained evidence ledger and resilience primitives (retry, circuit breaker, rate limiter).

如何安装 @forgrit/shared?

在项目目录运行 npm install @forgrit/shared,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 forgrit-ds,可以用 npx forgrit-ds 直接运行。

@forgrit/shared 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@forgrit/shared 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@forgrit/shared 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@forgrit/shared 可以商用吗?

@forgrit/shared 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@forgrit/shared 的下载量有多少?

根据 npm 官方下载统计,@forgrit/shared 在 2026/10/16 – 2026/11/14 这 30 天内被下载 2,847 次,日均约 95 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@forgrit/shared 还在维护吗?

最新版本 0.3.0 发布于 2026/09/26(1 个月前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/08/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。