@factlas/core
v0.4.0
Published
Deterministic, content-addressed design-system fact extraction & normalization engine
Downloads
1,514
Maintainers
Readme
@factlas/core
The deterministic fact layer for Factlas: a typed, normalized, content-addressed, auditable stream of design-system facts extracted from TypeScript/TSX and CSS by static analysis.
Core is not a database and not a linter. It parses source once, dispatches to technology plugins, normalizes their raw observations with versioned algorithms, and assigns each fact a content-addressed
fact_id.
See ADR-0001 for the full design and IMPLEMENTATION_PLAN.md for build status.
Status
Phase 1 (contract & determinism spine) — complete. Exposes:
- The Fact contract —
Fact(discriminated union over all 6 kinds), the envelope/subject/value types, andFACT_KINDS. - Version anchors —
FACT_SCHEMA_VERSION,NORMALIZER_VERSION. - Determinism spine —
canonicalStringify/sha256Hex, content-addressedcomputeFactId/factify, anddiscover(file discovery + hashing + the snapshot header with acache_key). - Published schema artifacts, both generated from the types (
npm run generate) and gated byFACT_SCHEMA_VERSION— a drift test keeps them in sync:@factlas/core/schema/fact.schema.json— the JSON Schema for a Fact.@factlas/core/schema/columns.json— a flat, DB-agnostic column manifest ({ name, path, type, nullable }) for the common fields, so you can generate a fact table in any database straight from what factlas ships (see DOWNSTREAM.md §1).
import {
discover,
factify,
FACT_SCHEMA_VERSION,
type Fact,
} from '@factlas/core';
const { files, header } = await discover({ root: process.cwd() });
// header.cache_key changes whenever any determinism input changesPhase 2 (parsing & plugin host) — complete. Adds:
- Base parsers —
parseModule(TS/TSX → Babel AST, parsed once) andparseStylesheet(CSS → PostCSS root), plustraversere-exported so plugins never depend on Babel directly, andbabelLoc/postcssLocconverters. - The plugin contract —
DesignFactsPlugin(analyzeCss/analyzeProgram),PluginContext(injectstokenize,resolve,parseCss,emit,diagnostic), and theObservationshape plugins emit (raw only — core owns normalization +fact_id). - Bounded resolver —
resolveExpression: one hop, literals only, no cross-file/node_modules, no execution; degrades to honestdynamic/unknown. - The extraction router —
extractFileroutes by extension, lifts CSS carriers back into the CSS path viactx.parseCss, and turns parse failures or throwing plugins into diagnostics rather than crashes.
import { extractFile, type DesignFactsPlugin } from '@factlas/core';
const { observations, diagnostics } = extractFile({ file, code, plugins });Phase 3 (normalization & assembly) — complete. Adds:
- Versioned normalizers (pure, gated by
NORMALIZER_VERSION):normalizeColor(culori → canonical hex),normalizeLength,normalizeKeyword,normalizeProperty(camelCase/vendor-prefix → kebab), and thenormalizeValuedispatcher — the single function facts and allowed-sets must share. classifyCertainty— the certainty decision tree (literal | static-union | dynamic | unknown).assembleFact/assembleFacts— turn a raw observation into a finalized, content-addressedFact: classify → normalize → canonicalize subject →factify, enforcing the invariants (dynamic/unknown ⇒norm: null+ a diagnostic; an unnormalizable literal degrades to an honestunknown, never a silent drop).sortFactsgives deterministic output ordering.
import { extractFile, assembleFacts, sortFacts } from '@factlas/core';
const extracted = extractFile({ file, code, plugins });
const facts = assembleFacts(extracted); // normalized, sorted FactsPhase 4 (default plugins) — complete. The five plugins now extract real facts end-to-end, verified by a golden-fixture byte-stability test:
@factlas/plugin-jsx—import+jsx.element/jsx.prop/jsx.attributefrom TS/TSX; ownselement_id.@factlas/plugin-css—css.declarationfrom stylesheets.@factlas/plugin-inline-style—css.declaration(sourceinline) fromstyle={{}}.@factlas/plugin-styled—css.declaration(sourcecss-in-js) from styled-components / emotion.@factlas/plugin-tailwind—css.classfrom TailwindclassNameusage (cn/clsx/cva, arbitrary values).
import { extractFile, assembleFacts, sortFacts } from '@factlas/core';
import css from '@factlas/plugin-css';
import inlineStyle from '@factlas/plugin-inline-style';
import styled from '@factlas/plugin-styled';
import tailwind from '@factlas/plugin-tailwind';
const plugins = [css, inlineStyle, styled, tailwind];
const facts = assembleFacts(extractFile({ file, code, plugins }));Phase 5 (repo orchestration) — complete. extractRepo({ root, plugins })
runs the whole fact layer over a repository (discover → extract → assemble →
globally-sorted facts) and is what @factlas/cli wraps:
import { extractRepo } from '@factlas/core';
const { header, facts, diagnostics } = await extractRepo({ root: './src', plugins });The in-scope project (the fact layer) is complete. Evaluation — store, policies, scoring, SARIF, gating — is intentionally downstream; see docs/DOWNSTREAM.md.
Install
npm install @factlas/core