@validation-os/api 0.10.0 → 0.12.0
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/dist/index.d.ts +3 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ import { AnyRecord, DataProvider } from '@validation-os/core';
|
|
|
16
16
|
* Stamp the derived values a reading owns:
|
|
17
17
|
* - the row's `derived.sourceQuality` (Representativeness × Credibility), a
|
|
18
18
|
* property of the artifact, shared by every belief it scores;
|
|
19
|
-
* - each belief's `derived.strength`
|
|
19
|
+
* - each belief's `derived.strength` = the ROW's rung anchor × sign(the
|
|
20
|
+
* belief's Result) [× the row's market magnitude band] (OPS 0.10 — one rung
|
|
21
|
+
* per artifact, per-belief Result);
|
|
20
22
|
* - `assumptionIds`, the projection of `beliefs[].assumptionId`, kept in sync
|
|
21
23
|
* whenever beliefs[] is written (mirrors barLineAssumptionIds).
|
|
22
24
|
* A partial patch that omits a field leaves that derivation untouched.
|
package/dist/index.js
CHANGED
|
@@ -27,10 +27,11 @@ function deriveReadingFields(data) {
|
|
|
27
27
|
const beliefs = r.beliefs.map((b) => ({
|
|
28
28
|
...b,
|
|
29
29
|
derived: {
|
|
30
|
+
// Rung + magnitude band are row-level; only the Result varies per belief.
|
|
30
31
|
strength: recomputeStrength({
|
|
31
|
-
rung:
|
|
32
|
+
rung: r.Rung,
|
|
32
33
|
result: b.Result,
|
|
33
|
-
magnitudeBand:
|
|
34
|
+
magnitudeBand: r.magnitudeBand
|
|
34
35
|
})
|
|
35
36
|
}
|
|
36
37
|
}));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/derive-on-write.ts"],"sourcesContent":["/**\n * @validation-os/api — framework-neutral route-handler logic.\n *\n * Handlers operate on the Web `Request`/`Response` types (what Next.js app-\n * router route handlers receive), so the same functions drop into a Next app\n * or any Web-standard runtime. Authentication is injected — the deployed app\n * supplies a Clerk-verifying `authenticate`, so this package stays free of any\n * auth-vendor dependency. Every write runs the derivation module server-side.\n */\nimport {\n REGISTERS,\n RELATIONS,\n isNotFoundError,\n isStaleVersionError,\n type Collection,\n type DataProvider,\n type RecordRef,\n type Relation,\n} from \"@validation-os/core\";\nimport {\n deriveReadingFields,\n recomputeAllDerived,\n} from \"./derive-on-write.js\";\n\nexport { deriveReadingFields, recomputeAllDerived };\n\nconst COLLECTIONS = new Set<string>(REGISTERS);\n/** Writes to these trigger the cross-record derived recompute pass. */\nconst DERIVED_TRIGGERS = new Set<string>([\"assumptions\", \"readings\", \"decisions\"]);\n\nexport interface AuthResult {\n /**\n * The raw, verified identity-provider subject (e.g. the Clerk user ID). The\n * package treats it as an opaque string and resolves it against the roster;\n * the auth vendor stays confined to the injected `authenticate`.\n */\n subject: string;\n}\n\n/**\n * One team member the register recognises — the `vocabulary.dashboard_users`\n * entry. `authSubject` is the IdP-neutral mapping a caller's token subject\n * resolves through; `name` is what lands in `Owner` / `Agreed by` (OPS-1343).\n */\nexport interface DashboardUser {\n name: string;\n authSubject: string;\n}\n\nexport interface CreateApiOptions {\n provider: DataProvider;\n /**\n * Verify the request's bearer token and return its subject, or null to\n * reject with 401. Roster resolution (subject → member) happens here in the\n * package, so `authenticate` carries no auth-vendor-specific mapping.\n */\n authenticate: (req: Request) => Promise<AuthResult | null> | AuthResult | null;\n /**\n * The known team roster. A caller whose subject resolves to a member may\n * write (any register); an unmapped subject is a 403. Sourced from\n * `vocabulary.dashboard_users` in the deployment config.\n */\n roster: DashboardUser[];\n /** Run derive-on-write after writes (default true). */\n deriveOnWrite?: boolean;\n}\n\n/** Registers that carry an `Owner`; glossary has none. */\nconst REGISTERS_WITH_OWNER = new Set<string>([\n \"assumptions\",\n \"experiments\",\n \"readings\",\n \"decisions\",\n]);\n\n/** True when a create supplied no Owner to stamp the caller into. */\nfunction ownerAbsent(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n (Array.isArray(value) && value.length === 0)\n );\n}\n\n/**\n * Every identity a write names (Owner, Agreed by) must be a roster member —\n * the body's word for *who* is never trusted. A stranger name is a 400.\n */\nfunction assertMembers(\n field: string,\n value: unknown,\n names: Set<string>,\n): void {\n if (value === undefined || value === null) return;\n const entries = Array.isArray(value) ? value : [value];\n for (const v of entries) {\n if (typeof v !== \"string\" || !names.has(v)) {\n throw new BadRequest(\n `${field} must reference a team member; \"${String(v)}\" is not one.`,\n );\n }\n }\n}\n\n/** Both identity fields a write may carry must name roster members. */\nfunction assertIdentityFields(\n data: Record<string, unknown>,\n names: Set<string>,\n): void {\n assertMembers(\"Owner\", data.Owner, names);\n assertMembers(\"Agreed by\", data[\"Agreed by\"], names);\n}\n\ntype Params = Record<string, string | undefined>;\nexport interface RouteContext {\n params: Params | Promise<Params>;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nasync function resolveParams(ctx: RouteContext): Promise<Params> {\n return await ctx.params;\n}\n\nfunction assertRegister(value: string | undefined): Collection {\n if (!value || !COLLECTIONS.has(value)) {\n throw new BadRequest(`Unknown register: ${String(value)}`);\n }\n return value as Collection;\n}\n\nclass BadRequest extends Error {}\n\ninterface LinkBody {\n relation: Relation;\n from: RecordRef;\n to: RecordRef;\n}\n\n/** Validate a POST /api/link body against the relation config. */\nfunction parseLinkBody(body: unknown): LinkBody {\n const b = body as Partial<{ relation: string; from: RecordRef; to: RecordRef }>;\n const relation = b.relation;\n if (typeof relation !== \"string\" || !(relation in RELATIONS)) {\n throw new BadRequest(`Unknown relation: ${String(relation)}`);\n }\n const spec = RELATIONS[relation as Relation];\n const from = parseRef(b.from, \"from\");\n const to = parseRef(b.to, \"to\");\n // The `from` end must be the register the relation config names; otherwise a\n // caller could write the wrong field. The `to` end is checked when it is a\n // stored (non-derived) end.\n if (from.register !== spec.from.register) {\n throw new BadRequest(\n `Relation ${relation} starts on ${spec.from.register}, not ${from.register}.`,\n );\n }\n if (to.register !== spec.targetRegister) {\n throw new BadRequest(\n `Relation ${relation} ends on ${spec.targetRegister}, not ${to.register}.`,\n );\n }\n return { relation: relation as Relation, from, to };\n}\n\nfunction parseRef(value: unknown, which: string): RecordRef {\n const ref = value as Partial<RecordRef> | undefined;\n if (!ref || typeof ref.id !== \"string\" || !ref.id) {\n throw new BadRequest(`Missing \\`${which}.id\\`.`);\n }\n return { register: assertRegister(ref.register), id: ref.id };\n}\n\n/** Turn a thrown error into the right HTTP response, in plain language. */\nfunction toErrorResponse(e: unknown): Response {\n if (e instanceof BadRequest) return json({ error: e.message }, 400);\n if (isNotFoundError(e)) return json({ error: e.message }, 404);\n if (isStaleVersionError(e)) {\n return json(\n {\n error: \"conflict\",\n // Plain-language copy — never version jargon (spec user story 12).\n message:\n \"Someone edited this while you had it open — your changes are \" +\n \"safe, take a look before saving again.\",\n },\n 409,\n );\n }\n return json({ error: \"internal\", message: \"Something went wrong.\" }, 500);\n}\n\nexport interface ValidationOsApi {\n /** GET /api/[register] — list every row of a register. */\n list(req: Request, ctx: RouteContext): Promise<Response>;\n /** GET /api/[register]/[id] — one record. */\n get(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/[register] — create; body is the record fields. */\n create(req: Request, ctx: RouteContext): Promise<Response>;\n /** PATCH /api/[register]/[id] — body { version, ...patch }. */\n update(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/link — body { relation, from, to }; sets both ends. */\n link(req: Request): Promise<Response>;\n /** POST /api/unlink — body { relation, from, to }; removes both ends. */\n unlink(req: Request): Promise<Response>;\n /** GET /api/counts — per-register row counts (the walking-skeleton value). */\n counts(req: Request): Promise<Response>;\n /** POST /api/recompute — run the derived-fields backstop pass. */\n recompute(req: Request): Promise<Response>;\n}\n\nexport function createApi(options: CreateApiOptions): ValidationOsApi {\n const { provider } = options;\n const deriveOn = options.deriveOnWrite ?? true;\n\n const memberNames = new Set(options.roster.map((u) => u.name));\n\n /**\n * The membership gate (OPS-1343): a request with no/invalid token is a 401;\n * a valid token whose subject is on no roster entry is a 403. Returns the\n * resolved member — one lookup is both gate and identity source.\n */\n async function guard(req: Request): Promise<{ name: string }> {\n const auth = await options.authenticate(req);\n if (!auth) throw new Unauthorized();\n const member = options.roster.find((u) => u.authSubject === auth.subject);\n if (!member) throw new Forbidden();\n return { name: member.name };\n }\n\n async function maybeRecompute(register: Collection): Promise<void> {\n if (deriveOn && DERIVED_TRIGGERS.has(register)) {\n await recomputeAllDerived(provider);\n }\n }\n\n /**\n * A relation can move a derived number (a reading joins a belief, a standing\n * decision lands, a dependency edge appears), so link/unlink run the same\n * backstop recompute the write routes use whenever either end is a trigger.\n */\n async function maybeRecomputeRelation(relation: Relation): Promise<void> {\n const spec = RELATIONS[relation];\n if (\n DERIVED_TRIGGERS.has(spec.from.register) ||\n (spec.to != null && DERIVED_TRIGGERS.has(spec.to.register))\n ) {\n await recomputeAllDerived(provider);\n }\n }\n\n return {\n async list(req, ctx) {\n try {\n await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n return json({ data: await provider.list(register) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async get(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n return json({ data: await provider.get(register, p.id) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async create(req, ctx) {\n try {\n const { name } = await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n let data = (await req.json()) as Record<string, unknown>;\n // Identity is stamped, not trusted: any Owner/Agreed-by the client sent\n // must be a roster member, and an omitted Owner defaults to the caller.\n assertIdentityFields(data, memberNames);\n if (REGISTERS_WITH_OWNER.has(register) && ownerAbsent(data.Owner)) {\n data = { ...data, Owner: [name] };\n }\n if (register === \"readings\") data = deriveReadingFields(data);\n const created = await provider.create(register, data);\n await maybeRecompute(register);\n return json({ data: created }, 201);\n } catch (e) {\n return handle(e);\n }\n },\n\n async update(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n const body = (await req.json()) as Record<string, unknown>;\n const { version, ...rawPatch } = body;\n if (typeof version !== \"number\") {\n throw new BadRequest(\"A numeric `version` is required for updates.\");\n }\n // An update may re-attribute Owner/Agreed-by, but only to roster members.\n assertIdentityFields(rawPatch, memberNames);\n const patch =\n register === \"readings\" ? deriveReadingFields(rawPatch) : rawPatch;\n const updated = await provider.update(register, p.id, patch, version);\n await maybeRecompute(register);\n return json({ data: updated });\n } catch (e) {\n return handle(e);\n }\n },\n\n async link(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.link(relation, from, to);\n await maybeRecomputeRelation(relation);\n return json({ data: { linked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async unlink(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.unlink(relation, from, to);\n await maybeRecomputeRelation(relation);\n return json({ data: { unlinked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async counts(req) {\n try {\n await guard(req);\n const registers = [...COLLECTIONS] as Collection[];\n const counts: Record<string, number> = {};\n await Promise.all(\n registers.map(async (r) => {\n counts[r] = (await provider.list(r)).length;\n }),\n );\n return json({ counts });\n } catch (e) {\n return handle(e);\n }\n },\n\n async recompute(req) {\n try {\n await guard(req);\n const updated = await recomputeAllDerived(provider);\n return json({ updated });\n } catch (e) {\n return handle(e);\n }\n },\n };\n}\n\nclass Unauthorized extends Error {}\n/** Authenticated, but the subject resolves to no roster member (OPS-1343). */\nclass Forbidden extends Error {}\n\nfunction handle(e: unknown): Response {\n if (e instanceof Unauthorized) {\n return json({ error: \"unauthorized\" }, 401);\n }\n if (e instanceof Forbidden) {\n return json(\n {\n error: \"forbidden\",\n message:\n \"Your account isn't on this register's team yet — ask an admin to \" +\n \"add you.\",\n },\n 403,\n );\n }\n return toErrorResponse(e);\n}\n","/**\n * Derive-on-write — the API runs the shared derivation module server-side so\n * stored derived values are always authoritative, whatever the client sent.\n *\n * Two layers:\n * - `deriveReadingFields`: a reading's own Source quality + Strength are pure\n * functions of the record itself — stamped inline on every reading write.\n * - `recomputeAllDerived`: the cross-record pass (Confidence, Derived Impact,\n * Risk over the whole assumptions register) — the backstop the spec keeps\n * for non-dashboard writes, run after any assumption/reading/decision write.\n */\nimport {\n recomputeDerived,\n recomputeSourceQuality,\n recomputeStrength,\n type AnyRecord,\n type AssumptionRecord,\n type BeliefScore,\n type DataProvider,\n type DecisionRecord,\n type ReadingRecord,\n} from \"@validation-os/core\";\n\n/**\n * Stamp the derived values a reading owns:\n * - the row's `derived.sourceQuality` (Representativeness × Credibility), a\n * property of the artifact, shared by every belief it scores;\n * - each belief's `derived.strength` (rung anchor × sign(Result) [× band]);\n * - `assumptionIds`, the projection of `beliefs[].assumptionId`, kept in sync\n * whenever beliefs[] is written (mirrors barLineAssumptionIds).\n * A partial patch that omits a field leaves that derivation untouched.\n */\nexport function deriveReadingFields(data: Partial<AnyRecord>): Partial<AnyRecord> {\n const r = data as Partial<ReadingRecord>;\n const out: Partial<AnyRecord> = { ...data };\n\n if (r.Representativeness != null && r.Credibility != null) {\n out.derived = {\n sourceQuality: recomputeSourceQuality(\n Number(r.Representativeness),\n Number(r.Credibility),\n ),\n };\n }\n\n if (Array.isArray(r.beliefs)) {\n const beliefs = r.beliefs.map((b) => ({\n ...b,\n derived: {\n strength: recomputeStrength({\n rung: b.Rung,\n result: b.Result,\n magnitudeBand: b.magnitudeBand,\n }),\n },\n })) as BeliefScore[];\n out.beliefs = beliefs;\n out.assumptionIds = beliefs.map((b) => b.assumptionId);\n }\n\n return out;\n}\n\n/**\n * Recompute Confidence / Derived Impact / Risk for every assumption and write\n * back only those whose derived tuple actually changed (so unrelated rows\n * don't churn their version). Returns the number of rows updated.\n */\nexport async function recomputeAllDerived(\n provider: DataProvider,\n): Promise<number> {\n const [assumptions, readings, decisions] = await Promise.all([\n provider.list(\"assumptions\"),\n provider.list(\"readings\"),\n provider.list(\"decisions\"),\n ]);\n\n const derived = recomputeDerived({\n assumptions: assumptions as unknown as AssumptionRecord[],\n readings: readings as unknown as ReadingRecord[],\n decisions: decisions as unknown as DecisionRecord[],\n });\n\n let updated = 0;\n for (const a of assumptions) {\n const next = derived.get(a.id);\n if (!next) continue;\n const cur = (a.derived ?? {}) as Partial<AssumptionRecord[\"derived\"]>;\n if (\n cur.confidence === next.confidence &&\n cur.risk === next.risk &&\n cur.derivedImpact === next.derivedImpact &&\n cur.completeness === next.completeness\n ) {\n continue;\n }\n await provider.update(\"assumptions\", a.id, { derived: next }, a.version);\n updated += 1;\n }\n return updated;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/derive-on-write.ts"],"sourcesContent":["/**\n * @validation-os/api — framework-neutral route-handler logic.\n *\n * Handlers operate on the Web `Request`/`Response` types (what Next.js app-\n * router route handlers receive), so the same functions drop into a Next app\n * or any Web-standard runtime. Authentication is injected — the deployed app\n * supplies a Clerk-verifying `authenticate`, so this package stays free of any\n * auth-vendor dependency. Every write runs the derivation module server-side.\n */\nimport {\n REGISTERS,\n RELATIONS,\n isNotFoundError,\n isStaleVersionError,\n type Collection,\n type DataProvider,\n type RecordRef,\n type Relation,\n} from \"@validation-os/core\";\nimport {\n deriveReadingFields,\n recomputeAllDerived,\n} from \"./derive-on-write.js\";\n\nexport { deriveReadingFields, recomputeAllDerived };\n\nconst COLLECTIONS = new Set<string>(REGISTERS);\n/** Writes to these trigger the cross-record derived recompute pass. */\nconst DERIVED_TRIGGERS = new Set<string>([\"assumptions\", \"readings\", \"decisions\"]);\n\nexport interface AuthResult {\n /**\n * The raw, verified identity-provider subject (e.g. the Clerk user ID). The\n * package treats it as an opaque string and resolves it against the roster;\n * the auth vendor stays confined to the injected `authenticate`.\n */\n subject: string;\n}\n\n/**\n * One team member the register recognises — the `vocabulary.dashboard_users`\n * entry. `authSubject` is the IdP-neutral mapping a caller's token subject\n * resolves through; `name` is what lands in `Owner` / `Agreed by` (OPS-1343).\n */\nexport interface DashboardUser {\n name: string;\n authSubject: string;\n}\n\nexport interface CreateApiOptions {\n provider: DataProvider;\n /**\n * Verify the request's bearer token and return its subject, or null to\n * reject with 401. Roster resolution (subject → member) happens here in the\n * package, so `authenticate` carries no auth-vendor-specific mapping.\n */\n authenticate: (req: Request) => Promise<AuthResult | null> | AuthResult | null;\n /**\n * The known team roster. A caller whose subject resolves to a member may\n * write (any register); an unmapped subject is a 403. Sourced from\n * `vocabulary.dashboard_users` in the deployment config.\n */\n roster: DashboardUser[];\n /** Run derive-on-write after writes (default true). */\n deriveOnWrite?: boolean;\n}\n\n/** Registers that carry an `Owner`; glossary has none. */\nconst REGISTERS_WITH_OWNER = new Set<string>([\n \"assumptions\",\n \"experiments\",\n \"readings\",\n \"decisions\",\n]);\n\n/** True when a create supplied no Owner to stamp the caller into. */\nfunction ownerAbsent(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n (Array.isArray(value) && value.length === 0)\n );\n}\n\n/**\n * Every identity a write names (Owner, Agreed by) must be a roster member —\n * the body's word for *who* is never trusted. A stranger name is a 400.\n */\nfunction assertMembers(\n field: string,\n value: unknown,\n names: Set<string>,\n): void {\n if (value === undefined || value === null) return;\n const entries = Array.isArray(value) ? value : [value];\n for (const v of entries) {\n if (typeof v !== \"string\" || !names.has(v)) {\n throw new BadRequest(\n `${field} must reference a team member; \"${String(v)}\" is not one.`,\n );\n }\n }\n}\n\n/** Both identity fields a write may carry must name roster members. */\nfunction assertIdentityFields(\n data: Record<string, unknown>,\n names: Set<string>,\n): void {\n assertMembers(\"Owner\", data.Owner, names);\n assertMembers(\"Agreed by\", data[\"Agreed by\"], names);\n}\n\ntype Params = Record<string, string | undefined>;\nexport interface RouteContext {\n params: Params | Promise<Params>;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nasync function resolveParams(ctx: RouteContext): Promise<Params> {\n return await ctx.params;\n}\n\nfunction assertRegister(value: string | undefined): Collection {\n if (!value || !COLLECTIONS.has(value)) {\n throw new BadRequest(`Unknown register: ${String(value)}`);\n }\n return value as Collection;\n}\n\nclass BadRequest extends Error {}\n\ninterface LinkBody {\n relation: Relation;\n from: RecordRef;\n to: RecordRef;\n}\n\n/** Validate a POST /api/link body against the relation config. */\nfunction parseLinkBody(body: unknown): LinkBody {\n const b = body as Partial<{ relation: string; from: RecordRef; to: RecordRef }>;\n const relation = b.relation;\n if (typeof relation !== \"string\" || !(relation in RELATIONS)) {\n throw new BadRequest(`Unknown relation: ${String(relation)}`);\n }\n const spec = RELATIONS[relation as Relation];\n const from = parseRef(b.from, \"from\");\n const to = parseRef(b.to, \"to\");\n // The `from` end must be the register the relation config names; otherwise a\n // caller could write the wrong field. The `to` end is checked when it is a\n // stored (non-derived) end.\n if (from.register !== spec.from.register) {\n throw new BadRequest(\n `Relation ${relation} starts on ${spec.from.register}, not ${from.register}.`,\n );\n }\n if (to.register !== spec.targetRegister) {\n throw new BadRequest(\n `Relation ${relation} ends on ${spec.targetRegister}, not ${to.register}.`,\n );\n }\n return { relation: relation as Relation, from, to };\n}\n\nfunction parseRef(value: unknown, which: string): RecordRef {\n const ref = value as Partial<RecordRef> | undefined;\n if (!ref || typeof ref.id !== \"string\" || !ref.id) {\n throw new BadRequest(`Missing \\`${which}.id\\`.`);\n }\n return { register: assertRegister(ref.register), id: ref.id };\n}\n\n/** Turn a thrown error into the right HTTP response, in plain language. */\nfunction toErrorResponse(e: unknown): Response {\n if (e instanceof BadRequest) return json({ error: e.message }, 400);\n if (isNotFoundError(e)) return json({ error: e.message }, 404);\n if (isStaleVersionError(e)) {\n return json(\n {\n error: \"conflict\",\n // Plain-language copy — never version jargon (spec user story 12).\n message:\n \"Someone edited this while you had it open — your changes are \" +\n \"safe, take a look before saving again.\",\n },\n 409,\n );\n }\n return json({ error: \"internal\", message: \"Something went wrong.\" }, 500);\n}\n\nexport interface ValidationOsApi {\n /** GET /api/[register] — list every row of a register. */\n list(req: Request, ctx: RouteContext): Promise<Response>;\n /** GET /api/[register]/[id] — one record. */\n get(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/[register] — create; body is the record fields. */\n create(req: Request, ctx: RouteContext): Promise<Response>;\n /** PATCH /api/[register]/[id] — body { version, ...patch }. */\n update(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/link — body { relation, from, to }; sets both ends. */\n link(req: Request): Promise<Response>;\n /** POST /api/unlink — body { relation, from, to }; removes both ends. */\n unlink(req: Request): Promise<Response>;\n /** GET /api/counts — per-register row counts (the walking-skeleton value). */\n counts(req: Request): Promise<Response>;\n /** POST /api/recompute — run the derived-fields backstop pass. */\n recompute(req: Request): Promise<Response>;\n}\n\nexport function createApi(options: CreateApiOptions): ValidationOsApi {\n const { provider } = options;\n const deriveOn = options.deriveOnWrite ?? true;\n\n const memberNames = new Set(options.roster.map((u) => u.name));\n\n /**\n * The membership gate (OPS-1343): a request with no/invalid token is a 401;\n * a valid token whose subject is on no roster entry is a 403. Returns the\n * resolved member — one lookup is both gate and identity source.\n */\n async function guard(req: Request): Promise<{ name: string }> {\n const auth = await options.authenticate(req);\n if (!auth) throw new Unauthorized();\n const member = options.roster.find((u) => u.authSubject === auth.subject);\n if (!member) throw new Forbidden();\n return { name: member.name };\n }\n\n async function maybeRecompute(register: Collection): Promise<void> {\n if (deriveOn && DERIVED_TRIGGERS.has(register)) {\n await recomputeAllDerived(provider);\n }\n }\n\n /**\n * A relation can move a derived number (a reading joins a belief, a standing\n * decision lands, a dependency edge appears), so link/unlink run the same\n * backstop recompute the write routes use whenever either end is a trigger.\n */\n async function maybeRecomputeRelation(relation: Relation): Promise<void> {\n const spec = RELATIONS[relation];\n if (\n DERIVED_TRIGGERS.has(spec.from.register) ||\n (spec.to != null && DERIVED_TRIGGERS.has(spec.to.register))\n ) {\n await recomputeAllDerived(provider);\n }\n }\n\n return {\n async list(req, ctx) {\n try {\n await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n return json({ data: await provider.list(register) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async get(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n return json({ data: await provider.get(register, p.id) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async create(req, ctx) {\n try {\n const { name } = await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n let data = (await req.json()) as Record<string, unknown>;\n // Identity is stamped, not trusted: any Owner/Agreed-by the client sent\n // must be a roster member, and an omitted Owner defaults to the caller.\n assertIdentityFields(data, memberNames);\n if (REGISTERS_WITH_OWNER.has(register) && ownerAbsent(data.Owner)) {\n data = { ...data, Owner: [name] };\n }\n if (register === \"readings\") data = deriveReadingFields(data);\n const created = await provider.create(register, data);\n await maybeRecompute(register);\n return json({ data: created }, 201);\n } catch (e) {\n return handle(e);\n }\n },\n\n async update(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n const body = (await req.json()) as Record<string, unknown>;\n const { version, ...rawPatch } = body;\n if (typeof version !== \"number\") {\n throw new BadRequest(\"A numeric `version` is required for updates.\");\n }\n // An update may re-attribute Owner/Agreed-by, but only to roster members.\n assertIdentityFields(rawPatch, memberNames);\n const patch =\n register === \"readings\" ? deriveReadingFields(rawPatch) : rawPatch;\n const updated = await provider.update(register, p.id, patch, version);\n await maybeRecompute(register);\n return json({ data: updated });\n } catch (e) {\n return handle(e);\n }\n },\n\n async link(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.link(relation, from, to);\n await maybeRecomputeRelation(relation);\n return json({ data: { linked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async unlink(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.unlink(relation, from, to);\n await maybeRecomputeRelation(relation);\n return json({ data: { unlinked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async counts(req) {\n try {\n await guard(req);\n const registers = [...COLLECTIONS] as Collection[];\n const counts: Record<string, number> = {};\n await Promise.all(\n registers.map(async (r) => {\n counts[r] = (await provider.list(r)).length;\n }),\n );\n return json({ counts });\n } catch (e) {\n return handle(e);\n }\n },\n\n async recompute(req) {\n try {\n await guard(req);\n const updated = await recomputeAllDerived(provider);\n return json({ updated });\n } catch (e) {\n return handle(e);\n }\n },\n };\n}\n\nclass Unauthorized extends Error {}\n/** Authenticated, but the subject resolves to no roster member (OPS-1343). */\nclass Forbidden extends Error {}\n\nfunction handle(e: unknown): Response {\n if (e instanceof Unauthorized) {\n return json({ error: \"unauthorized\" }, 401);\n }\n if (e instanceof Forbidden) {\n return json(\n {\n error: \"forbidden\",\n message:\n \"Your account isn't on this register's team yet — ask an admin to \" +\n \"add you.\",\n },\n 403,\n );\n }\n return toErrorResponse(e);\n}\n","/**\n * Derive-on-write — the API runs the shared derivation module server-side so\n * stored derived values are always authoritative, whatever the client sent.\n *\n * Two layers:\n * - `deriveReadingFields`: a reading's own Source quality + Strength are pure\n * functions of the record itself — stamped inline on every reading write.\n * - `recomputeAllDerived`: the cross-record pass (Confidence, Derived Impact,\n * Risk over the whole assumptions register) — the backstop the spec keeps\n * for non-dashboard writes, run after any assumption/reading/decision write.\n */\nimport {\n recomputeDerived,\n recomputeSourceQuality,\n recomputeStrength,\n type AnyRecord,\n type AssumptionRecord,\n type BeliefScore,\n type DataProvider,\n type DecisionRecord,\n type ReadingRecord,\n} from \"@validation-os/core\";\n\n/**\n * Stamp the derived values a reading owns:\n * - the row's `derived.sourceQuality` (Representativeness × Credibility), a\n * property of the artifact, shared by every belief it scores;\n * - each belief's `derived.strength` = the ROW's rung anchor × sign(the\n * belief's Result) [× the row's market magnitude band] (OPS 0.10 — one rung\n * per artifact, per-belief Result);\n * - `assumptionIds`, the projection of `beliefs[].assumptionId`, kept in sync\n * whenever beliefs[] is written (mirrors barLineAssumptionIds).\n * A partial patch that omits a field leaves that derivation untouched.\n */\nexport function deriveReadingFields(data: Partial<AnyRecord>): Partial<AnyRecord> {\n const r = data as Partial<ReadingRecord>;\n const out: Partial<AnyRecord> = { ...data };\n\n if (r.Representativeness != null && r.Credibility != null) {\n out.derived = {\n sourceQuality: recomputeSourceQuality(\n Number(r.Representativeness),\n Number(r.Credibility),\n ),\n };\n }\n\n if (Array.isArray(r.beliefs)) {\n const beliefs = r.beliefs.map((b) => ({\n ...b,\n derived: {\n // Rung + magnitude band are row-level; only the Result varies per belief.\n strength: recomputeStrength({\n rung: r.Rung!,\n result: b.Result,\n magnitudeBand: r.magnitudeBand,\n }),\n },\n })) as BeliefScore[];\n out.beliefs = beliefs;\n out.assumptionIds = beliefs.map((b) => b.assumptionId);\n }\n\n return out;\n}\n\n/**\n * Recompute Confidence / Derived Impact / Risk for every assumption and write\n * back only those whose derived tuple actually changed (so unrelated rows\n * don't churn their version). Returns the number of rows updated.\n */\nexport async function recomputeAllDerived(\n provider: DataProvider,\n): Promise<number> {\n const [assumptions, readings, decisions] = await Promise.all([\n provider.list(\"assumptions\"),\n provider.list(\"readings\"),\n provider.list(\"decisions\"),\n ]);\n\n const derived = recomputeDerived({\n assumptions: assumptions as unknown as AssumptionRecord[],\n readings: readings as unknown as ReadingRecord[],\n decisions: decisions as unknown as DecisionRecord[],\n });\n\n let updated = 0;\n for (const a of assumptions) {\n const next = derived.get(a.id);\n if (!next) continue;\n const cur = (a.derived ?? {}) as Partial<AssumptionRecord[\"derived\"]>;\n if (\n cur.confidence === next.confidence &&\n cur.risk === next.risk &&\n cur.derivedImpact === next.derivedImpact &&\n cur.completeness === next.completeness\n ) {\n continue;\n }\n await provider.update(\"assumptions\", a.id, { derived: next }, a.version);\n updated += 1;\n }\n return updated;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@validation-os/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Framework-neutral route-handler logic for validation-os: auth-guarded CRUD over a DataProvider, with derive-on-write via the shared derivation module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@validation-os/core": "0.
|
|
24
|
+
"@validation-os/core": "0.12.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsup": "^8.3.0",
|