@blamejs/blamejs-shop 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/currency-display.js +201 -4
- package/lib/storefront.js +612 -80
- package/lib/translations.js +203 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/ai-aedt-bias-audit.js +180 -0
- package/lib/vendor/blamejs/lib/ai-frontier-protocol.js +196 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.5.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.13.6.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-aedt-bias-audit.test.js +107 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-frontier-protocol.test.js +83 -0
- package/package.json +1 -1
package/lib/translations.js
CHANGED
|
@@ -537,10 +537,213 @@ function _hydrate(row) {
|
|
|
537
537
|
};
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
// ---- storefront UI chrome catalog --------------------------------------
|
|
541
|
+
//
|
|
542
|
+
// The chrome strings the storefront renders on EVERY page — nav links,
|
|
543
|
+
// search controls, footer columns + links, the newsletter band, the
|
|
544
|
+
// locale switcher. These are the default-locale (`en`) baseline; an
|
|
545
|
+
// operator localises any key by authoring a `translations` row under
|
|
546
|
+
// resource_kind `ui`, resource_id `chrome`, field = the dotted key with
|
|
547
|
+
// dots rewritten to underscores (the table's `field` slug grammar
|
|
548
|
+
// forbids dots), locale = the target tag. `resolveChrome` reads those
|
|
549
|
+
// overrides at boot per active locale and layers them over this baseline,
|
|
550
|
+
// so an untranslated key always renders the English string — never a
|
|
551
|
+
// raw key. The exact English values here are the literal strings the
|
|
552
|
+
// layout shipped inline before i18n landed, so an `en` storefront is
|
|
553
|
+
// byte-identical to the pre-i18n markup.
|
|
554
|
+
//
|
|
555
|
+
// Both render paths (the container `lib/storefront.js` and the edge
|
|
556
|
+
// `worker/render/*`) consume this same catalog through `b.i18n`, so a
|
|
557
|
+
// given locale resolves to one identical string set on both sides.
|
|
558
|
+
var CHROME_KEY_PREFIX = "ui.chrome.";
|
|
559
|
+
var CHROME_RESOURCE_KIND = "ui";
|
|
560
|
+
var CHROME_RESOURCE_ID = "chrome";
|
|
561
|
+
|
|
562
|
+
var CHROME_DEFAULTS = Object.freeze({
|
|
563
|
+
skip_to_content: "Skip to content",
|
|
564
|
+
|
|
565
|
+
util_pill: "Open source · Apache 2.0",
|
|
566
|
+
util_msg: "Server-rendered HTML · post-quantum crypto on by default · zero npm runtime deps",
|
|
567
|
+
util_star: "Star on GitHub →",
|
|
568
|
+
|
|
569
|
+
search_label: "Search products",
|
|
570
|
+
search_placeholder: "Search the catalog",
|
|
571
|
+
search_submit: "Search",
|
|
572
|
+
|
|
573
|
+
nav_shop: "Shop",
|
|
574
|
+
nav_framework: "Framework",
|
|
575
|
+
nav_account: "Account",
|
|
576
|
+
// The cart aria-label carries the live item count; `{count}` is
|
|
577
|
+
// interpolated by b.i18n at render time.
|
|
578
|
+
nav_cart_aria: "Cart, {count} items",
|
|
579
|
+
|
|
580
|
+
newsletter_eyebrow: "Stay in the loop",
|
|
581
|
+
newsletter_title: "Get release notes the day they ship.",
|
|
582
|
+
newsletter_lede: "No marketing emails. A single short note when there's a new framework release, a security advisory, or a primitive worth knowing about.",
|
|
583
|
+
newsletter_email: "Email address",
|
|
584
|
+
newsletter_submit: "Subscribe",
|
|
585
|
+
|
|
586
|
+
footer_tagline: "An open-source shop framework — server-rendered HTML, zero npm runtime dependencies, security defaults on.",
|
|
587
|
+
|
|
588
|
+
footer_shop_heading: "Shop",
|
|
589
|
+
footer_shop_all: "All products",
|
|
590
|
+
footer_shop_collections: "Collections",
|
|
591
|
+
footer_shop_categories: "Categories",
|
|
592
|
+
footer_shop_new: "New arrivals",
|
|
593
|
+
footer_shop_sale: "On sale",
|
|
594
|
+
footer_shop_compare: "Compare",
|
|
595
|
+
footer_shop_cart: "Cart",
|
|
596
|
+
|
|
597
|
+
footer_framework_heading: "Framework",
|
|
598
|
+
footer_framework_source: "Source on GitHub",
|
|
599
|
+
footer_framework_core: "blamejs core",
|
|
600
|
+
footer_framework_security: "Security policy",
|
|
601
|
+
footer_framework_changelog: "Changelog",
|
|
602
|
+
|
|
603
|
+
footer_operators_heading: "Operators",
|
|
604
|
+
footer_operators_account: "Account",
|
|
605
|
+
footer_operators_orders: "Orders",
|
|
606
|
+
footer_operators_admin: "Admin",
|
|
607
|
+
footer_operators_contact: "Contact",
|
|
608
|
+
|
|
609
|
+
footer_copy_suffix: "built on blamejs · Apache 2.0 licensed.",
|
|
610
|
+
footer_legal_security: "Security",
|
|
611
|
+
footer_legal_privacy: "Privacy",
|
|
612
|
+
footer_legal_terms: "Terms",
|
|
613
|
+
footer_legal_cookies: "Manage cookies",
|
|
614
|
+
|
|
615
|
+
locale_switcher_label: "Language",
|
|
616
|
+
locale_switcher_submit: "Go",
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Default English chrome strings exposed verbatim for the render paths
|
|
620
|
+
// to fall back on when no i18n instance is configured (the storefront
|
|
621
|
+
// stays browsable on a deploy that hasn't seeded a locale policy).
|
|
622
|
+
function chromeDefaults() {
|
|
623
|
+
return Object.assign({}, CHROME_DEFAULTS);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Build a `b.i18n` instance carrying the chrome catalog for every active
|
|
627
|
+
// locale. The baseline `en` tree is the shipped defaults; each other
|
|
628
|
+
// locale's tree starts empty and is layered with the operator's `ui`
|
|
629
|
+
// translation rows (read once here). b.i18n's own lookup chain then
|
|
630
|
+
// falls a missing key through to `defaultLocale` (the shipped English),
|
|
631
|
+
// so a partially-translated locale never surfaces a raw key.
|
|
632
|
+
//
|
|
633
|
+
// `opts.overrides` is the pre-read DB override map
|
|
634
|
+
// ({ "<locale>": { "<field>": "<value>", ... } }) — passed in by the
|
|
635
|
+
// caller (the storefront mount reads it via `readChromeOverrides`)
|
|
636
|
+
// rather than read here, so this stays a synchronous, query-free build
|
|
637
|
+
// that the edge Worker can run with the same inputs as the container.
|
|
638
|
+
function createChromeI18n(opts) {
|
|
639
|
+
opts = opts || {};
|
|
640
|
+
var defaultLocale = opts.defaultLocale || BASELINE_LOCALE;
|
|
641
|
+
var locales = Array.isArray(opts.locales) && opts.locales.length
|
|
642
|
+
? opts.locales.slice()
|
|
643
|
+
: [defaultLocale];
|
|
644
|
+
if (locales.indexOf(defaultLocale) === -1) locales.unshift(defaultLocale);
|
|
645
|
+
var overrides = opts.overrides || {};
|
|
646
|
+
|
|
647
|
+
// Each locale's inline tree under the single `ui.chrome.*` namespace.
|
|
648
|
+
// The default locale gets the full English baseline; other locales get
|
|
649
|
+
// only their authored overrides (the rest falls through b.i18n's chain
|
|
650
|
+
// to the default locale).
|
|
651
|
+
var translations = {};
|
|
652
|
+
for (var li = 0; li < locales.length; li += 1) {
|
|
653
|
+
var loc = locales[li];
|
|
654
|
+
var chrome = {};
|
|
655
|
+
if (loc === defaultLocale) {
|
|
656
|
+
var defKeys = Object.keys(CHROME_DEFAULTS);
|
|
657
|
+
for (var d = 0; d < defKeys.length; d += 1) chrome[defKeys[d]] = CHROME_DEFAULTS[defKeys[d]];
|
|
658
|
+
}
|
|
659
|
+
var ovr = overrides[loc];
|
|
660
|
+
if (ovr) {
|
|
661
|
+
var ovrKeys = Object.keys(ovr);
|
|
662
|
+
for (var o = 0; o < ovrKeys.length; o += 1) {
|
|
663
|
+
// Only keys we know about land in the tree — an operator typo
|
|
664
|
+
// (a field that isn't a chrome key) is ignored rather than
|
|
665
|
+
// shadowing a real string with garbage.
|
|
666
|
+
if (Object.prototype.hasOwnProperty.call(CHROME_DEFAULTS, ovrKeys[o])) {
|
|
667
|
+
chrome[ovrKeys[o]] = ovr[ovrKeys[o]];
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
translations[loc] = { ui: { chrome: chrome } };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return b.i18n.create({
|
|
675
|
+
defaultLocale: defaultLocale,
|
|
676
|
+
locales: locales,
|
|
677
|
+
fallbackLocale: defaultLocale,
|
|
678
|
+
translations: translations,
|
|
679
|
+
// A missing chrome key returns the English baseline via the chain;
|
|
680
|
+
// should a key be absent from every locale (only possible if the
|
|
681
|
+
// baseline catalog itself is edited), return the dotted key rather
|
|
682
|
+
// than throwing inside a render. The render path never shows it
|
|
683
|
+
// because every shipped key has an `en` baseline.
|
|
684
|
+
missingKey: "return-key",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Resolve the full chrome string set for one locale into a flat object
|
|
689
|
+
// keyed by the chrome field names (`nav_shop`, `footer_shop_all`, …),
|
|
690
|
+
// each value already locale-resolved through the i18n chain. The render
|
|
691
|
+
// paths feed this straight into the layout placeholders. The
|
|
692
|
+
// `nav_cart_aria` string keeps its literal `{count}` placeholder — the
|
|
693
|
+
// render path interpolates the live cart count uniformly across both
|
|
694
|
+
// substrates, so this stays request-independent and cacheable.
|
|
695
|
+
function resolveChrome(i18n, locale) {
|
|
696
|
+
var out = {};
|
|
697
|
+
var keys = Object.keys(CHROME_DEFAULTS);
|
|
698
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
699
|
+
var key = keys[i];
|
|
700
|
+
// No interpolation vars — `{count}` (and any future placeholder)
|
|
701
|
+
// survives as a literal for the render path to fill.
|
|
702
|
+
out[key] = i18n.t(CHROME_KEY_PREFIX + key, {}, { locale: locale });
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Read the operator's `ui`/`chrome` override rows for the given locales
|
|
708
|
+
// into the `{ "<locale>": { "<field>": "<value>" } }` shape
|
|
709
|
+
// `createChromeI18n` consumes. Missing-table-resilient: a deploy whose
|
|
710
|
+
// `translations` table hasn't been migrated (or a transient read error)
|
|
711
|
+
// yields an empty override map, so the storefront falls back to the
|
|
712
|
+
// English baseline rather than 500-ing. The caller passes a `query`
|
|
713
|
+
// function (the externalDb handle).
|
|
714
|
+
async function readChromeOverrides(query, locales) {
|
|
715
|
+
if (typeof query !== "function" || !Array.isArray(locales) || !locales.length) return {};
|
|
716
|
+
var out = {};
|
|
717
|
+
try {
|
|
718
|
+
var placeholders = locales.map(function (_l, i) { return "?" + (i + 3); }).join(", ");
|
|
719
|
+
var rows = (await query(
|
|
720
|
+
"SELECT locale, field, value FROM translations " +
|
|
721
|
+
"WHERE resource_kind = ?1 AND resource_id = ?2 AND locale IN (" + placeholders + ")",
|
|
722
|
+
[CHROME_RESOURCE_KIND, CHROME_RESOURCE_ID].concat(locales)
|
|
723
|
+
)).rows;
|
|
724
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
725
|
+
var r = rows[i];
|
|
726
|
+
if (!out[r.locale]) out[r.locale] = {};
|
|
727
|
+
out[r.locale][r.field] = r.value;
|
|
728
|
+
}
|
|
729
|
+
} catch (_e) {
|
|
730
|
+
// Missing table / read failure — degrade to the English baseline.
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
|
|
540
736
|
module.exports = {
|
|
541
737
|
create: create,
|
|
542
738
|
MAX_VALUE_LEN: MAX_VALUE_LEN,
|
|
543
739
|
MAX_BULK_ROWS: MAX_BULK_ROWS,
|
|
544
740
|
BASELINE_LOCALE: BASELINE_LOCALE,
|
|
545
741
|
DEFAULT_SAMPLE_SIZE: DEFAULT_SAMPLE_SIZE,
|
|
742
|
+
CHROME_DEFAULTS: CHROME_DEFAULTS,
|
|
743
|
+
CHROME_RESOURCE_KIND: CHROME_RESOURCE_KIND,
|
|
744
|
+
CHROME_RESOURCE_ID: CHROME_RESOURCE_ID,
|
|
745
|
+
chromeDefaults: chromeDefaults,
|
|
746
|
+
createChromeI18n: createChromeI18n,
|
|
747
|
+
resolveChrome: resolveChrome,
|
|
748
|
+
readChromeOverrides: readChromeOverrides,
|
|
546
749
|
};
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.6",
|
|
7
|
+
"tag": "v0.13.6",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.6 (2026-05-26) — **`b.ai.frontierModelProtocol` — California SB 53 frontier-AI obligations.** b.ai.frontierModelProtocol assesses a developer's obligations under California's Transparency in Frontier Artificial Intelligence Act — SB 53, Cal. Bus. & Prof. Code §22757.10, effective 2026-01-01 — from a model's training compute and the developer's revenue. It reports whether the model crosses the frontier threshold (more than 10^26 training FLOPs), whether the developer is a large frontier developer (prior-year revenue, with affiliates, above $500M), and the resulting obligations: every frontier developer must report critical safety incidents and publish a transparency report, and a large frontier developer must additionally publish an annual safety framework and disclose its catastrophic-risk assessment. Passing a candidate safety framework reports which required elements (risk identification, mitigation, governance, cybersecurity, standards alignment) are missing. b.ai.frontierModelProtocol.incidentReport validates a critical-incident type against the Act's four categories and computes the notification deadline to the California Office of Emergency Services — 15 days from discovery, or 24 hours when there is an imminent risk of death or serious physical injury. The ca-tfaia compliance posture was already in the catalog. **Added:** *`b.ai.frontierModelProtocol` — SB 53 threshold classification, obligations, and incident reporting* — `b.ai.frontierModelProtocol({ trainingFlops, annualRevenueUsd, framework? })` returns `isFrontierModel`, `isLargeFrontierDeveloper`, the `obligations` list, and (when a framework is supplied) its `frameworkGaps`. `b.ai.frontierModelProtocol.incidentReport({ type, discoveredAt, imminentRiskToLife? })` builds a critical-safety-incident report with the California OES recipient and a `dueAt` / `deadlineHours` computed from the 15-day (or 24-hour imminent-risk) statutory window; `type` must be one of the four categories in `INCIDENT_TYPES`. Throws `FrontierProtocolError` on malformed input.
|
|
12
|
+
|
|
13
|
+
- v0.13.5 (2026-05-26) — **`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit.** b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog. **Added:** *`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios* — `b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `"selection"` (group entries `{ selected, total }`) or `"scoring"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input.
|
|
14
|
+
|
|
11
15
|
- v0.13.4 (2026-05-26) — **`b.crdt` — conflict-free replicated data types.** b.crdt adds state-based Conflict-free Replicated Data Types: data structures that independent replicas update without coordination and still converge to the same value once they have exchanged state. Each type's merge is a join over a semilattice — commutative, associative, and idempotent — so replicas can merge in any order, any number of times, and agree, which makes these the substrate for active/active cluster state, offline-first clients that reconcile on reconnect, and eventually-consistent counters, sets, and maps. The release ships the full state-based family: grow-only and positive-negative counters (gCounter / pnCounter), grow-only, two-phase, and observed-remove sets (gSet / twoPSet / orSet), a last-write-wins register (lwwRegister), and an observed-remove map (orMap). Every type exposes the same contract — local mutators, merge(other) that returns a converged instance without mutating either operand, value() for the materialized value, and state() / fromState() for a JSON-serializable form to snapshot via b.archive or b.backup or ship to a peer — and carries a replicaId so per-replica contributions stay distinct. **Added:** *`b.crdt` — state-based CvRDT counters, sets, register, and map* — `b.crdt.gCounter` / `pnCounter` (grow-only and increment/decrement counters), `b.crdt.gSet` / `twoPSet` / `orSet` (grow-only, two-phase, and observed-remove sets — `orSet` supports re-add and resolves a concurrent add-vs-remove as add-wins), `b.crdt.lwwRegister` (last-write-wins with a deterministic replicaId tie-break), and `b.crdt.orMap` (observed-remove keys with last-write-wins values). Each exposes `merge` / `value` / `state` / `fromState` and converges by the CvRDT laws. `orSet` and `orMap` accept `tombstoneRetention` to bound tombstone memory against a remove flood.
|
|
12
16
|
|
|
13
17
|
- v0.13.3 (2026-05-26) — **`b.crypto.xwing` — X-Wing hybrid post-quantum KEM.** b.crypto.xwing adds the X-Wing hybrid key-encapsulation mechanism (draft-connolly-cfrg-xwing-kem): it runs ML-KEM-768 and X25519 side by side and binds their shared secrets with SHA3-256, so an encapsulated key stays secure as long as either ML-KEM-768 or X25519 holds. That is the conservative shape for moving off classical ECDH today — a harvest-now-decrypt-later attacker must break the lattice KEM, and a hypothetical ML-KEM break still leaves X25519 standing. keygen() produces a 32-byte decapsulation seed and a 1216-byte public key; encapsulate(publicKey) returns a 1120-byte ciphertext and a 32-byte shared secret; decapsulate(secretKey, ciphertext) recovers it. The X-Wing combiner is frozen, but its specification is still an IETF Internet-Draft, so this primitive is marked experimental and sits beside the existing pre-RFC post-quantum HPKE drafts; it composes the framework's vendored ML-KEM-768 and X25519 with SHA3 and adds no new cryptographic core. The combiner is known-answer-tested byte-for-byte against the draft's definition. **Added:** *`b.crypto.xwing` — X-Wing hybrid PQ/T KEM (experimental)* — `keygen(seed?)` → `{ publicKey (1216 B), secretKey (32-byte seed) }`; `encapsulate(publicKey, eseed?)` → `{ ciphertext (1120 B), sharedSecret (32 B) }`; `decapsulate(secretKey, ciphertext)` → the 32-byte shared secret. Both `keygen` and `encapsulate` accept an optional seed for deterministic operation. The combiner — `SHA3-256(ssMLKEM ‖ ssX25519 ‖ ctX25519 ‖ pkX25519 ‖ label)` — is exposed as `combiner` for advanced use. Marked `experimental` while draft-connolly-cfrg-xwing-kem remains an Internet-Draft; the algorithm itself is frozen.
|
|
@@ -189,6 +189,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
189
189
|
- **Content provenance** — C2PA 2.1 + California SB-942 / AB-853 manifest builder for AI-generated media (provider, model id + version, timestamp, content ID, signed) (`b.contentCredentials`)
|
|
190
190
|
- **AI usage quotas** — per-tenant / per-model budgets metered by tokens / requests / cost-usd / compute-hours over calendar-aligned windows, with an atomic conditional reserve (no charge-then-refund race) + hard/soft/warn enforcement and an optional cross-node store; defends OWASP LLM10:2025 unbounded consumption / denial-of-wallet (`b.ai.quota`)
|
|
191
191
|
- **AI capability routing** — model-capability registry (context window / modalities / tool use / reasoning tier / cost rates) + a router that picks the cheapest model satisfying a request's requirements, refusing capability mismatches before the inference call (NIST AI RMF MAP + Model Cards); composes with `b.ai.quota` cost budgets (`b.ai.capability`)
|
|
192
|
+
- **AEDT bias audit** — NYC Local Law 144 bias-audit figures (`b.ai.aedtBiasAudit`): selection / scoring rates and EEOC four-fifths-rule impact ratios across sex, race/ethnicity, and their intersection, with the most-selected group and adverse-impact flags (impact ratio < 0.8) for the annual published summary; sub-2% categories excludable per DCWP §5-301
|
|
193
|
+
- **Frontier AI protocol** — California SB 53 (Transparency in Frontier AI Act) obligations (`b.ai.frontierModelProtocol`): classify the frontier-model (>10²⁶ training FLOPs) and large-frontier-developer (>$500M revenue) thresholds, enumerate the resulting obligations, check a safety framework for required elements, and build a critical-safety-incident report with the 15-day / 24-hour California OES notification deadline (`.incidentReport`)
|
|
192
194
|
### Compliance regimes
|
|
193
195
|
|
|
194
196
|
- **Posture coordinator** — `b.compliance` cascades operator-declared regime into retention / audit / db / cryptoField via POSTURE_DEFAULTS:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.13.
|
|
4
|
-
"createdAt": "2026-05-
|
|
3
|
+
"frameworkVersion": "0.13.6",
|
|
4
|
+
"createdAt": "2026-05-27T02:34:13.690Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -1388,6 +1388,10 @@
|
|
|
1388
1388
|
}
|
|
1389
1389
|
}
|
|
1390
1390
|
},
|
|
1391
|
+
"aedtBiasAudit": {
|
|
1392
|
+
"type": "function",
|
|
1393
|
+
"arity": 1
|
|
1394
|
+
},
|
|
1391
1395
|
"aiContentDetect": {
|
|
1392
1396
|
"type": "object",
|
|
1393
1397
|
"members": {
|
|
@@ -1573,6 +1577,10 @@
|
|
|
1573
1577
|
}
|
|
1574
1578
|
}
|
|
1575
1579
|
},
|
|
1580
|
+
"frontierModelProtocol": {
|
|
1581
|
+
"type": "function",
|
|
1582
|
+
"arity": 1
|
|
1583
|
+
},
|
|
1576
1584
|
"input": {
|
|
1577
1585
|
"type": "object",
|
|
1578
1586
|
"members": {
|
|
@@ -55,6 +55,7 @@ var KNOWN_POSTURES = {
|
|
|
55
55
|
// v0.10.8 — EU AI Act Art. 50 + Art. 11 + AB-853 + CAC + AI governance
|
|
56
56
|
"eu-ai-act-art-50": 1, "eu-ai-act-art-11": 1,
|
|
57
57
|
"ca-ab-853": 1, "ca-sb-942": 1,
|
|
58
|
+
"nyc-ll144": 1, "ca-tfaia": 1,
|
|
58
59
|
"cac-genai-label": 1,
|
|
59
60
|
"nist-ai-600-1": 1, "nist-ai-rmf": 1,
|
|
60
61
|
"iso-42001": 1, "iso-23894": 1,
|
|
@@ -477,6 +477,8 @@ module.exports = {
|
|
|
477
477
|
quota: require("./lib/ai-quota"),
|
|
478
478
|
capability: require("./lib/ai-capability"),
|
|
479
479
|
dp: require("./lib/ai-dp"),
|
|
480
|
+
aedtBiasAudit: require("./lib/ai-aedt-bias-audit"),
|
|
481
|
+
frontierModelProtocol: require("./lib/ai-frontier-protocol"),
|
|
480
482
|
},
|
|
481
483
|
promisePool: require("./lib/promise-pool"),
|
|
482
484
|
sdNotify: require("./lib/sd-notify"),
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.ai.aedtBiasAudit
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title AEDT Bias Audit
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Compute the bias-audit statistics New York City Local Law 144 requires
|
|
9
|
+
* before an Automated Employment Decision Tool (AEDT) may be used to screen
|
|
10
|
+
* candidates or employees. The law (NYC Admin. Code §20-870 et seq., in force
|
|
11
|
+
* since 2023-07-05; DCWP rules 6 RCNY §5-300 et seq.) requires an independent
|
|
12
|
+
* annual audit that reports, for each demographic category, the rate at which
|
|
13
|
+
* the tool selects (or scores above the median for) that group and the
|
|
14
|
+
* <em>impact ratio</em> — that group's rate divided by the rate of the
|
|
15
|
+
* most-selected group. An impact ratio below the four-fifths (0.8) threshold
|
|
16
|
+
* from the EEOC Uniform Guidelines flags potential adverse impact; the law
|
|
17
|
+
* requires the number to be calculated and published, not any particular
|
|
18
|
+
* remediation.
|
|
19
|
+
*
|
|
20
|
+
* The audit is computed across three dimensions: sex, race/ethnicity, and
|
|
21
|
+
* the intersection of the two, using the EEOC categories. Categories that
|
|
22
|
+
* make up less than 2% of the audited data may be excluded from the impact-
|
|
23
|
+
* ratio calculation at the auditor's discretion (DCWP §5-301). This primitive
|
|
24
|
+
* takes the per-category counts — selected/total for a pass-fail tool, or
|
|
25
|
+
* scored-above-the-overall-median/total for a continuous-score tool — and
|
|
26
|
+
* returns the selection (or scoring) rate, impact ratio, and adverse-impact
|
|
27
|
+
* flag per group, plus the most-selected group and an overall flag. It is a
|
|
28
|
+
* pure calculation: the operator supplies the data an independent auditor
|
|
29
|
+
* collected, and gets back the figures the published summary must contain.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* NYC Local Law 144 AEDT bias audit (`b.ai.aedtBiasAudit`) — selection /
|
|
33
|
+
* scoring rates and four-fifths-rule impact ratios across sex, race/ethnicity,
|
|
34
|
+
* and their intersection, with the most-selected group and adverse-impact
|
|
35
|
+
* flags for the published audit summary.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var validateOpts = require("./validate-opts");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var AedtBiasAuditError = defineClass("AedtBiasAuditError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
var FOUR_FIFTHS = 4 / 5; // EEOC Uniform Guidelines four-fifths adverse-impact threshold
|
|
44
|
+
var DEFAULT_MIN_SHARE = 0.02; // DCWP §5-301 — categories under 2% may be excluded
|
|
45
|
+
var DIMENSIONS = ["sex", "raceEthnicity", "intersectional"];
|
|
46
|
+
|
|
47
|
+
function _str(v, label) {
|
|
48
|
+
if (typeof v !== "string" || v.length === 0) throw new AedtBiasAuditError("aedt/bad-metadata", "aedtBiasAudit: metadata." + label + " must be a non-empty string");
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
function _count(v, label) {
|
|
52
|
+
if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) throw new AedtBiasAuditError("aedt/bad-count", "aedtBiasAudit: " + label + " must be a non-negative integer");
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Reduce one dimension's per-group counts to the LL-144 figures.
|
|
57
|
+
function _auditDimension(groups, type, minShare) {
|
|
58
|
+
var names = Object.keys(groups);
|
|
59
|
+
var rows = [];
|
|
60
|
+
var dimensionTotal = 0;
|
|
61
|
+
var numeratorKey = type === "scoring" ? "scoredAboveMedian" : "selected";
|
|
62
|
+
|
|
63
|
+
names.forEach(function (name) {
|
|
64
|
+
var g = groups[name];
|
|
65
|
+
if (!g || typeof g !== "object") throw new AedtBiasAuditError("aedt/bad-count", "aedtBiasAudit: group '" + name + "' must be an object with " + numeratorKey + " + total");
|
|
66
|
+
var total = _count(g.total, name + ".total");
|
|
67
|
+
var num = _count(g[numeratorKey], name + "." + numeratorKey);
|
|
68
|
+
if (num > total) throw new AedtBiasAuditError("aedt/bad-count", "aedtBiasAudit: " + name + "." + numeratorKey + " (" + num + ") exceeds total (" + total + ")");
|
|
69
|
+
dimensionTotal += total;
|
|
70
|
+
rows.push({ category: name, total: total, _num: num, rate: total === 0 ? 0 : num / total });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Exclude sub-threshold categories (auditor discretion), then find the
|
|
74
|
+
// most-selected group among those that remain.
|
|
75
|
+
var maxRate = 0;
|
|
76
|
+
var mostSelected = null;
|
|
77
|
+
rows.forEach(function (r) {
|
|
78
|
+
r.share = dimensionTotal === 0 ? 0 : r.total / dimensionTotal;
|
|
79
|
+
r.excluded = dimensionTotal > 0 && r.share < minShare;
|
|
80
|
+
if (!r.excluded && r.rate > maxRate) { maxRate = r.rate; mostSelected = r.category; }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
rows.forEach(function (r) {
|
|
84
|
+
r.impactRatio = (r.excluded || maxRate === 0) ? null : r.rate / maxRate;
|
|
85
|
+
r.adverseImpact = r.impactRatio !== null && r.impactRatio < FOUR_FIFTHS;
|
|
86
|
+
delete r._num;
|
|
87
|
+
});
|
|
88
|
+
// Stable order: highest rate first, then category name.
|
|
89
|
+
rows.sort(function (a, b) { return b.rate - a.rate || (a.category < b.category ? -1 : a.category > b.category ? 1 : 0); });
|
|
90
|
+
return { rows: rows, mostSelected: mostSelected };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @primitive b.ai.aedtBiasAudit
|
|
95
|
+
* @signature b.ai.aedtBiasAudit(opts)
|
|
96
|
+
* @since 0.13.5
|
|
97
|
+
* @status stable
|
|
98
|
+
* @compliance nyc-ll144, soc2
|
|
99
|
+
* @related b.ai.disclosure.applyAll, b.ai.disclosure.chatbot
|
|
100
|
+
*
|
|
101
|
+
* Compute the NYC Local Law 144 bias-audit figures from per-category counts.
|
|
102
|
+
* <code>type</code> is <code>"selection"</code> for a pass-fail tool (each
|
|
103
|
+
* group entry is <code>{ selected, total }</code>) or <code>"scoring"</code>
|
|
104
|
+
* for a continuous-score tool (<code>{ scoredAboveMedian, total }</code>, where
|
|
105
|
+
* the count is candidates scoring above the <em>overall</em> median). Returns
|
|
106
|
+
* the selection/scoring rate, impact ratio (group rate ÷ most-selected group's
|
|
107
|
+
* rate), and an <code>adverseImpact</code> flag (impact ratio < 0.8) per
|
|
108
|
+
* group, across the <code>sex</code>, <code>raceEthnicity</code>, and
|
|
109
|
+
* <code>intersectional</code> dimensions, plus the most-selected group per
|
|
110
|
+
* dimension. Categories under <code>minCategoryShare</code> (2% by default) are
|
|
111
|
+
* marked <code>excluded</code> and left out of the impact-ratio basis. Throws
|
|
112
|
+
* <code>AedtBiasAuditError</code> on malformed input. The result is the data an
|
|
113
|
+
* employer must publish; the law mandates the calculation, not any remediation.
|
|
114
|
+
*
|
|
115
|
+
* @opts
|
|
116
|
+
* type: string, // "selection" | "scoring" (required)
|
|
117
|
+
* metadata: object, // { tool, auditor, auditDate, distributionDate? } (tool/auditor/auditDate required)
|
|
118
|
+
* categories: object, // { sex?, raceEthnicity?, intersectional? } → { <group>: { selected|scoredAboveMedian, total } }
|
|
119
|
+
* minCategoryShare: number, // default: 0.02 (DCWP §5-301 — sub-2% categories may be excluded)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* var report = b.ai.aedtBiasAudit({
|
|
123
|
+
* type: "selection",
|
|
124
|
+
* metadata: { tool: "ResumeRanker v3", auditor: "Acme Audit LLC", auditDate: "2026-05-26" },
|
|
125
|
+
* categories: { sex: { Male: { selected: 60, total: 100 }, Female: { selected: 42, total: 100 } } },
|
|
126
|
+
* });
|
|
127
|
+
* report.results.sex[1].impactRatio; // → 0.7 (Female: 42% / 60%)
|
|
128
|
+
* report.results.sex[1].adverseImpact; // → true (below the 0.8 four-fifths threshold)
|
|
129
|
+
*/
|
|
130
|
+
function aedtBiasAudit(opts) {
|
|
131
|
+
opts = opts || {};
|
|
132
|
+
// Surface an unknown/typoed option as this primitive's own error type rather
|
|
133
|
+
// than the generic Error validateOpts throws, so the malformed-input contract
|
|
134
|
+
// (AedtBiasAuditError / e.code) holds for every bad-config path.
|
|
135
|
+
try { validateOpts(opts, ["type", "metadata", "categories", "minCategoryShare"], "aedtBiasAudit"); }
|
|
136
|
+
catch (e) { throw new AedtBiasAuditError("aedt/bad-opts", e && e.message || "aedtBiasAudit: invalid options"); }
|
|
137
|
+
if (opts.type !== "selection" && opts.type !== "scoring") throw new AedtBiasAuditError("aedt/bad-type", "aedtBiasAudit: type must be 'selection' or 'scoring'");
|
|
138
|
+
|
|
139
|
+
var md = opts.metadata || {};
|
|
140
|
+
var metadata = {
|
|
141
|
+
tool: _str(md.tool, "tool"),
|
|
142
|
+
auditor: _str(md.auditor, "auditor"),
|
|
143
|
+
auditDate: _str(md.auditDate, "auditDate"),
|
|
144
|
+
distributionDate: md.distributionDate != null ? _str(md.distributionDate, "distributionDate") : null,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
var minShare = opts.minCategoryShare != null ? opts.minCategoryShare : DEFAULT_MIN_SHARE;
|
|
148
|
+
if (typeof minShare !== "number" || !isFinite(minShare) || minShare < 0 || minShare >= 1) throw new AedtBiasAuditError("aedt/bad-share", "aedtBiasAudit: minCategoryShare must be a number in [0, 1)");
|
|
149
|
+
|
|
150
|
+
var cats = opts.categories || {};
|
|
151
|
+
var present = DIMENSIONS.filter(function (d) { return cats[d] && typeof cats[d] === "object" && Object.keys(cats[d]).length > 0; });
|
|
152
|
+
if (present.length === 0) throw new AedtBiasAuditError("aedt/no-categories", "aedtBiasAudit: at least one of sex / raceEthnicity / intersectional must carry group counts");
|
|
153
|
+
|
|
154
|
+
var results = {};
|
|
155
|
+
var mostSelected = {};
|
|
156
|
+
var adverseImpactGroups = [];
|
|
157
|
+
present.forEach(function (dim) {
|
|
158
|
+
var out = _auditDimension(cats[dim], opts.type, minShare);
|
|
159
|
+
results[dim] = out.rows;
|
|
160
|
+
mostSelected[dim] = out.mostSelected;
|
|
161
|
+
out.rows.forEach(function (r) { if (r.adverseImpact) adverseImpactGroups.push({ dimension: dim, category: r.category, impactRatio: r.impactRatio }); });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
type: opts.type,
|
|
166
|
+
metadata: metadata,
|
|
167
|
+
results: results,
|
|
168
|
+
summary: {
|
|
169
|
+
mostSelected: mostSelected,
|
|
170
|
+
adverseImpactGroups: adverseImpactGroups,
|
|
171
|
+
anyAdverseImpact: adverseImpactGroups.length > 0,
|
|
172
|
+
fourFifthsThreshold: FOUR_FIFTHS,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
aedtBiasAudit.FOUR_FIFTHS = FOUR_FIFTHS;
|
|
178
|
+
aedtBiasAudit.AedtBiasAuditError = AedtBiasAuditError;
|
|
179
|
+
|
|
180
|
+
module.exports = aedtBiasAudit;
|