npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/manufacturing

v0.1.0

Published

Manufacturing engine — immutable BOM revisions, work-order lifecycle with crash-healing stock sagas, append-only production ledger, isomorphic pure domain core (explosion, scheduling) that also runs in the browser. MongoDB persistence via @classytic/mongo

Readme

@classytic/manufacturing

Headless BOM + Work-Order kernel for Node.js, backed by Mongoose/mongokit — no HTTP, no auth. Build a make-to-order or make-to-stock workflow with multi-level BOM explosion, versioned/immutable BOM revisions, routing operations, and a Work-Order lifecycle with crash-safe atomic state transitions. An optional StockPort bridges cleanly to @classytic/flow (or your own WMS) — the kernel never assumes one is present.

  • Engine-factory kernel. createManufacturing({ connection, tenant, bridges }) returns an engine exposing repositories.boms / bomRevisions / workOrders / productionEvents. No in-memory adapters ship — this is a Mongo-backed kernel like the rest of the @classytic/* fleet.
  • Immutable BOM revisions, enforced at the data layer. Once a revision is active, every write path (not just the service guard) refuses via immutableStatesPlugin — direct update/delete/updateMany on an active revision throw RevisionImmutableError.
  • Atomic state transitions. Every BOM / Work-Order state change is a CAS against the expected status, so concurrent workers never double-release or double-complete. release() and complete() claim the new status before touching stock, so a losing racer is rejected before any reservation or consumption runs.
  • Multi-level BOM explosion. Sub-assemblies expand into leaf components, aggregated across paths, with cycle detection and a depth guard.
  • Routing operations. A BOM revision's operations steps land on the work order at plan() time; startOperation/completeOperation/ skipOperation enforce sequence order.
  • Crash healing. Stock-touching verbs stamp the exact request onto pendingStock in the same CAS write; healPendingStock() re-fires a stranded hand-off idempotently (via batchRef).
  • Events out of the box. manufacturing:bom.* / manufacturing:work_order.*.

Install

npm install @classytic/manufacturing

Peers: @classytic/primitives, @classytic/mongokit, @classytic/repo-core, mongoose.

Make-to-order, end-to-end

import mongoose from 'mongoose';
import { createManufacturing } from '@classytic/manufacturing';

const connection = mongoose.createConnection(process.env.MONGO_URI!);
const engine = await createManufacturing({
  connection,
  tenant: { tenantField: 'organizationId', fieldType: 'string', required: true },
  bridges: { stock: myStockPort }, // optional — omit for open-loop (no inventory mutation)
});

const ctx = { organizationId: 'org-1', actorId: 'engineer-1' };
const { boms, bomRevisions, workOrders } = engine.repositories;

// 1. Engineer publishes an active default BOM.
const { bom, revision } = await boms.createBom(
  {
    productSku: 'CHAIR',
    isDefault: true,
    content: {
      outputQuantity: 1,
      components: [
        { itemSku: 'LEG', quantity: 4 },
        { itemSku: 'SEAT', quantity: 1 },
        { itemSku: 'SCREW', quantity: 8, scrapPct: 25 },
      ],
    },
  },
  ctx,
);
await boms.activateRevision(String(bom._id), String(revision._id), ctx);

// 2. Sales accepts an order — host triggers make-to-order.
const wo = await workOrders.createWorkOrder({ productSku: 'CHAIR', plannedQuantity: 10 }, ctx);
await workOrders.plan(String(wo._id), ctx);     // explodes the active revision onto component lines
await workOrders.release(String(wo._id), ctx);  // reserves components via bridges.stock
await workOrders.start(String(wo._id), { by: 'operator' }, ctx);
await workOrders.complete(String(wo._id), { producedQuantity: 10 }, ctx); // consumes + produces

Engineering changes clone a revision rather than mutate an active one:

const rev2 = await boms.createRevision(String(bom._id), { changeNote: 'thicker screws' }, ctx);
await boms.activateRevision(String(bom._id), String(rev2._id), ctx); // supersedes rev 1

BOM explosion

plan() calls this internally; it is also exported as a pure function:

import { explodeBom, mapLookup } from '@classytic/manufacturing';

const requirements = await explodeBom(
  mainBom,
  qty,
  mapLookup([mainBom, subAssyBom]), // multi-level: pass a SubBomLookup
  { includeScrap: true, maxDepth: 16 },
);
  • Linear qty scaling.
  • scrapPct per line — (1 + scrapPct/100) uplift when includeScrap: true.
  • Duplicate SKUs across paths collapse to one ComponentRequirement with pathCount > 1.
  • Cycles throw CircularBomError with the offending cycle trace.
  • Depth past maxDepth throws BomDepthExceededError.
  • outputQuantity on a BOM divides the parent qty (e.g. one batch produces 5 units).
  • isOptional lines are included by default; the flag survives aggregation only when EVERY contributing path was optional.
  • substitutes (alternative SKUs) are carried through as data, unioned across paths — the kernel never auto-substitutes; hosts swap the SKU before release.

BOM types

Bom.type is normal (default) | phantom | kit:

| Type | Explosion | Work orders | |---|---|---| | normal | components as declared | allowed | | phantom | looked through to its components (never stocked) | NonProducibleBomError | | kit | components as declared — exploded at fulfillment by the HOST | NonProducibleBomError |

Routing operations

A BOM revision's operations (sequence + operation name + optional workCenterRef/expectedDurationMinutes) land on the work order as pending steps at plan() time — no separate routing repository:

await boms.createBom(
  {
    productSku: 'CHAIR',
    isDefault: true,
    content: { /* ...components */ },
    operations: [
      { sequence: 10, operation: 'cut', workCenterRef: 'wc_cut', expectedDurationMinutes: 30 },
      { sequence: 20, operation: 'weld', workCenterRef: 'wc_weld' },
      { sequence: 30, operation: 'assemble' },
    ],
  },
  ctx,
);

// while the work order is in_progress:
await workOrders.startOperation(String(wo._id), 10, { by: 'ali' }, ctx);
await workOrders.completeOperation(String(wo._id), 10, {}, ctx);
await workOrders.skipOperation(String(wo._id), 20, { by: 'supervisor', note: 'pre-welded batch' }, ctx);

Operations execute in sequence order — starting one while a lower-sequence operation is unfinished throws OperationSequenceError. complete() refuses while any operation is not done/skipped (OperationsIncompleteError) unless you pass allowIncompleteOperations: true. workCenterRef is an opaque string: capacity, scheduling and cost-per-hour belong to the host or a planner above this kernel.

Work-order state machine

draft → planned → released → in_progress → completed
    ↘      ↘         ↘            ↘
     cancelled (from any non-terminal state)

Each transition is CAS-guarded and appends to statusHistory. Invalid transitions throw InvalidTransitionError.

| Verb | What happens | Stock side effect | |---|---|---| | createWorkOrder | Persist draft, no stock touched | none | | plan | Explode the active BOM revision, write component + operation lines | none | | release | Reserve each component (if bridges.stock present) | reserve(...) per line | | start | Mark in_progress, record actualStart | none | | complete | Consume reservations, produce finished goods | consume(...) + produce(...) | | cancel | Abort at any non-terminal state | release(...) outstanding reservations |

Partial runs

Passing producedQuantity < plannedQuantity to complete() prorates component consumption. producedQuantity > plannedQuantity (over-completion) and negative quantities are rejected with OverCompletionError, never silently clamped.

Crash healing — pendingStock + healPendingStock()

Every stock-touching verb stamps the exact requests it is about to fire into WorkOrder.pendingStock in the same atomic CAS write that claims the status, and clears the stamp when the port call succeeds. If the process dies — or the stock port throws — in between, the stamp survives on the doc:

// Sweep (cron / startup): finish any stranded hand-off.
const stranded = await workOrders.getAll({ /* pendingStock != null */ }, ctx);
for (const wo of stranded.docs) await workOrders.healPendingStock(String(wo._id), ctx);

healPendingStock() re-fires the stored requests, clears the stamp, and emits manufacturing:work_order.stock_healednot the original released/completed/cancelled event, so a downstream consumer that keys off the lifecycle event (invoicing, FG receipt) must also subscribe to the heal event if it needs to observe a healed transition. Every request in a batch carries the original attempt's batchRef; a durable StockPort adapter MUST treat it as an idempotency key so a heal that follows a partially executed attempt never double-applies.

Stock integration

StockPort is a four/five-method interface the kernel calls at release / complete / cancel time:

interface StockPort {
  reserve(requests): Promise<StockReservationResult[]>;
  release(reservationRefs): Promise<void>;
  consume(requests): Promise<void>;       // handles partial-consume
  produce(requests): Promise<void>;
  checkAvailability?(skus): Promise<StockAvailability[]>; // optional preflight
}

Omit bridges.stock entirely to run "open-loop" — work orders progress through their lifecycle with no inventory mutation. For production, write a thin adapter mapping StockPort onto @classytic/flow (or your WMS) — bridges are optional integrations, not bundled here.

Events

| Event | When | Payload | |---|---|---| | manufacturing:bom.created | createBom | { bomId, productSku } | | manufacturing:bom.activated | activateRevision | { bomId, revisionId } | | manufacturing:bom.archived | archiveBom | { bomId } | | manufacturing:bom_revision.created | createRevision | { bomId, revisionId } | | manufacturing:bom_revision.activated | activateRevision | { bomId, revisionId } | | manufacturing:bom_revision.archived | archiveRevision | { bomId, revisionId } | | manufacturing:work_order.created | createWorkOrder | { workOrderId, bomId, productSku, plannedQuantity } | | manufacturing:work_order.planned | plan | { workOrderId, componentCount, operationCount } | | manufacturing:work_order.released | release | { workOrderId } | | manufacturing:work_order.started | start | { workOrderId, actualStart } | | manufacturing:work_order.completed | complete | { workOrderId, producedQuantity, actualEnd } | | manufacturing:work_order.cancelled | cancel | { workOrderId, reason } | | manufacturing:work_order.stock_healed | healPendingStock | { workOrderId, verb } | | manufacturing:work_order.operation_started/completed/skipped | *Operation | { workOrderId, sequence } |

What this kernel does NOT do (v0.1.0)

  • No cost roll-up. No Money/minor-units type anywhere — material, labor, and overhead cost roll-up, standard-vs-actual costing, and WIP valuation are not modeled. This is the largest gap for a full ERP manufacturing module; a costing layer is expected in a later release.
  • No by-product / co-product production. revision.byproducts is stored and cloned across revisions but never yields a produce() call at completion — declared by-products currently produce no stock.
  • No subcontracting beyond the NonProducibleBomError seam.
  • No master production schedule / capacity planning. That belongs in a planner above this kernel (see @classytic/planning).
  • No UoM conversion. uom is carried through but never converted — hosts pin one measure per SKU.
  • Production ledger is best-effort, not transactional. productionEvents append happens outside the state-write transaction; a transient write failure logs and continues rather than rolling back the work-order state. Don't treat the ledger as a strict source of truth for WIP-as-of-date queries until this is hardened.

Known limitation — test coverage

The current suite is 15 tests (unit + integration), covering the BOM revision lifecycle, immutability enforcement, and one full make-to-order work-order path. It does not yet exercise: cancel()'s stock-release path, archiveBom/archiveRevision, setDefault exclusivity, approval-gated activation, the consume_produce/release_refs heal kinds (only the reserve heal is tested), or concurrent-CAS races. Treat this as an early release — the engine core (saga discipline, immutable revisions, tenancy) is solid, but breadth of test coverage has not caught up to the verb surface.

Architecture

src/
├── index.ts                          ← main barrel
├── domain/
│   ├── entities/                     ← Bom, WorkOrder, ComponentRequirement
│   ├── enums/                        ← bom-status / bom-type / work-order-status / operation-status
│   ├── ports/                        ← StockPort (optional bridge), context
│   ├── pure/                         ← explodeBom + mapLookup (isomorphic, no I/O)
│   └── errors.ts
├── engine/
│   ├── create-manufacturing.ts       ← engine factory
│   └── engine-types.ts
├── models/                           ← Mongoose schemas (Bom, BomRevision, WorkOrder, ProductionEvent)
├── repositories/                     ← BomRepository, BomRevisionRepository, WorkOrderRepository, ProductionEventRepository
├── schemas/                          ← Zod input validation
└── events/
    └── event-constants.ts            ← MANUFACTURING_EVENTS map

Tests

npm test              # unit + integration
npm run test:unit     # unit only
npm run test:integration

License

See LICENSE.