@vantageos/backend-doctor 0.1.13 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +19 -2
  2. package/dist/args.d.ts +16 -0
  3. package/dist/args.js +54 -0
  4. package/dist/build-stamp.d.ts +13 -0
  5. package/dist/build-stamp.js +76 -0
  6. package/dist/build-stamp.json +4 -0
  7. package/dist/caller-resolution.d.ts +222 -0
  8. package/dist/caller-resolution.js +1195 -0
  9. package/dist/cli.js +217 -10
  10. package/dist/coverage-cli.js +8 -0
  11. package/dist/crud-coverage.d.ts +81 -0
  12. package/dist/crud-coverage.js +375 -0
  13. package/dist/detectors/oracle-axis1.d.ts +5 -3
  14. package/dist/detectors/oracle-axis1.js +24 -15
  15. package/dist/detectors/oracle-axis2.js +5 -6
  16. package/dist/detectors/predicates.d.ts +3 -0
  17. package/dist/detectors/predicates.js +81 -16
  18. package/dist/detectors/shared.js +5 -5
  19. package/dist/detectors/source/_scan.js +6 -1
  20. package/dist/detectors/source/backfill.js +9 -1
  21. package/dist/detectors/source/guard-helper-flow.d.ts +112 -0
  22. package/dist/detectors/source/guard-helper-flow.js +1173 -0
  23. package/dist/detectors/source/identity.d.ts +51 -2
  24. package/dist/detectors/source/identity.js +815 -14
  25. package/dist/detectors/source/isolation.d.ts +11 -1
  26. package/dist/detectors/source/isolation.js +106 -25
  27. package/dist/detectors/source/jobs.js +165 -2
  28. package/dist/detectors/source/readbudget.d.ts +1 -0
  29. package/dist/detectors/source/readbudget.js +67 -4
  30. package/dist/detectors/source/security.d.ts +16 -0
  31. package/dist/detectors/source/security.js +225 -7
  32. package/dist/detectors/source/write-isolation.d.ts +8 -1
  33. package/dist/detectors/source/write-isolation.js +64 -30
  34. package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
  35. package/dist/detectors/standard/r46-returns-validator.js +45 -14
  36. package/dist/detectors/structural.js +14 -10
  37. package/dist/oracle.d.ts +92 -3
  38. package/dist/oracle.js +229 -12
  39. package/dist/registry.d.ts +7 -0
  40. package/dist/registry.js +48 -6
  41. package/dist/report.d.ts +11 -1
  42. package/dist/report.js +15 -2
  43. package/dist/state-rank.d.ts +5 -0
  44. package/dist/state-rank.js +9 -0
  45. package/dist/tally.d.ts +35 -0
  46. package/dist/tally.js +76 -8
  47. package/dist/target.d.ts +32 -0
  48. package/dist/target.js +173 -0
  49. package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
  50. package/fixtures/cr1-classes/surface.ts +206 -0
  51. package/fixtures/cr1-empty/nothing.ts +1 -0
  52. package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
  53. package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
  54. package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
  55. package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
  56. package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
  57. package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
  58. package/package.json +2 -2
@@ -0,0 +1,180 @@
1
+ // CR-1 fixture — public doors gated the way vantage-peers 58ed649 gates the OKF
2
+ // export/import doors (okfBundleNode.ts:244-276, okfBundleDurable.ts:110-132,
3
+ // okfBundleNode.ts:783-793), plus the negative poles that must stay FAIL-OPEN.
4
+ // Each member is a pole for tests/cr1-refusal-on-resolved.test.ts.
5
+ import {
6
+ requireMasterDirect,
7
+ requireNamespaceSometimes,
8
+ requireNonEmpty,
9
+ requireTenantNamespace,
10
+ withOrgScope,
11
+ } from "./lib/auth";
12
+
13
+ // ─── helpers, product-shaped ─────────────────────────────────────────────────
14
+
15
+ /** ACTION side (okfBundleNode.ts:assertCanExportNamespace): binding through
16
+ * ctx.runQuery(internal.…resolveOrgScopeForAction), refusal through a
17
+ * TRANSITIVE value refuser. */
18
+ async function assertCanExportNamespace(
19
+ ctx: ActionCtx,
20
+ namespace: string,
21
+ door: string,
22
+ ): Promise<void> {
23
+ if (namespace.includes("..")) {
24
+ throw new Error(`OKF_NAMESPACE_INVALID: "${namespace}"`);
25
+ }
26
+ const scope = await ctx.runQuery(
27
+ internal.lib.auth.resolveOrgScopeForAction,
28
+ {},
29
+ );
30
+ requireTenantNamespace(scope, namespace, door);
31
+ }
32
+
33
+ /** okfBundleNode.ts:assertCanImport — reaches the gate one call deeper. */
34
+ async function assertCanImport(
35
+ ctx: ActionCtx,
36
+ namespace: string,
37
+ ): Promise<void> {
38
+ await assertCanExportNamespace(ctx, namespace, "doors:importBundle");
39
+ }
40
+
41
+ /** V8 side (okfBundleDurable.ts:assertCanExportNamespaceV8): binding through
42
+ * withOrgScope, refusal through the same TRANSITIVE value refuser. */
43
+ async function assertCanExportNamespaceV8(
44
+ ctx: MutationCtx,
45
+ namespace: string,
46
+ door: string,
47
+ ): Promise<void> {
48
+ const scope = await withOrgScope(ctx, { refuseWithoutThrow: true });
49
+ requireTenantNamespace(scope, namespace, door);
50
+ }
51
+
52
+ /** binding through withOrgScope, refusal through a DIRECT (one-level) refuser. */
53
+ async function assertMasterDirect(
54
+ ctx: MutationCtx,
55
+ door: string,
56
+ ): Promise<void> {
57
+ const scope = await withOrgScope(ctx);
58
+ requireMasterDirect(scope, door);
59
+ }
60
+
61
+ /** NEGATIVE 1 — same resolution, result IGNORED: the refuser is handed the
62
+ * namespace, never the resolved scope. */
63
+ async function assertIgnored(
64
+ ctx: ActionCtx,
65
+ namespace: string,
66
+ door: string,
67
+ ): Promise<void> {
68
+ // biome-ignore lint/correctness/noUnusedVariables: the UNUSED binding IS the pole.
69
+ const scope = await ctx.runQuery(
70
+ internal.lib.auth.resolveOrgScopeForAction,
71
+ {},
72
+ );
73
+ requireNonEmpty(namespace, door);
74
+ }
75
+
76
+ /** NEGATIVE 2 — the old okfBundleNode.ts:assertCanValidate shape: the
77
+ * absent-identity branch is a bare `return;`, whatever follows it. */
78
+ async function assertCanValidateOld(
79
+ ctx: ActionCtx,
80
+ namespace: string,
81
+ door: string,
82
+ ): Promise<void> {
83
+ const identity = await ctx.auth.getUserIdentity();
84
+ if (identity === null || identity === undefined) {
85
+ return;
86
+ }
87
+ const scope = await ctx.runQuery(
88
+ internal.lib.auth.resolveOrgScopeForAction,
89
+ {},
90
+ );
91
+ requireTenantNamespace(scope, namespace, door);
92
+ }
93
+
94
+ /** NEGATIVE 3 — the refuser proceeds silently on a caller-supplied value. */
95
+ async function assertSometimes(
96
+ ctx: MutationCtx,
97
+ namespace: string,
98
+ door: string,
99
+ ): Promise<void> {
100
+ const scope = await withOrgScope(ctx);
101
+ requireNamespaceSometimes(scope, namespace, door);
102
+ }
103
+
104
+ export const readRows = internalQuery({
105
+ args: { namespace: v.string() },
106
+ handler: async (ctx, args) => {
107
+ return await ctx.db
108
+ .query("rows")
109
+ .withIndex("by_namespace", (q) => q.eq("namespace", args.namespace))
110
+ .collect();
111
+ },
112
+ });
113
+
114
+ // ─── POSITIVE — RESOLVES ─────────────────────────────────────────────────────
115
+
116
+ export const exportBundle = action({
117
+ args: { namespace: v.string() },
118
+ handler: async (ctx, args) => {
119
+ await assertCanExportNamespace(ctx, args.namespace, "doors:exportBundle");
120
+ return await ctx.runQuery(internal.doors.readRows, {
121
+ namespace: args.namespace,
122
+ });
123
+ },
124
+ });
125
+
126
+ export const importBundle = action({
127
+ args: { namespace: v.string() },
128
+ handler: async (ctx, args) => {
129
+ await assertCanImport(ctx, args.namespace);
130
+ return await ctx.runQuery(internal.doors.readRows, {
131
+ namespace: args.namespace,
132
+ });
133
+ },
134
+ });
135
+
136
+ export const startExport = mutation({
137
+ args: { namespace: v.string() },
138
+ handler: async (ctx, args) => {
139
+ await assertCanExportNamespaceV8(ctx, args.namespace, "doors:startExport");
140
+ return await ctx.db.insert("jobs", { namespace: args.namespace });
141
+ },
142
+ });
143
+
144
+ export const directDoor = mutation({
145
+ args: { namespace: v.string() },
146
+ handler: async (ctx, args) => {
147
+ await assertMasterDirect(ctx, "doors:directDoor");
148
+ return await ctx.db.insert("jobs", { namespace: args.namespace });
149
+ },
150
+ });
151
+
152
+ // ─── NEGATIVE — FAIL-OPEN ────────────────────────────────────────────────────
153
+
154
+ export const ignoredDoor = action({
155
+ args: { namespace: v.string() },
156
+ handler: async (ctx, args) => {
157
+ await assertIgnored(ctx, args.namespace, "doors:ignoredDoor");
158
+ return await ctx.runQuery(internal.doors.readRows, {
159
+ namespace: args.namespace,
160
+ });
161
+ },
162
+ });
163
+
164
+ export const validateDoor = action({
165
+ args: { namespace: v.string() },
166
+ handler: async (ctx, args) => {
167
+ await assertCanValidateOld(ctx, args.namespace, "doors:validateDoor");
168
+ return await ctx.runQuery(internal.doors.readRows, {
169
+ namespace: args.namespace,
170
+ });
171
+ },
172
+ });
173
+
174
+ export const sometimesDoor = mutation({
175
+ args: { namespace: v.string() },
176
+ handler: async (ctx, args) => {
177
+ await assertSometimes(ctx, args.namespace, "doors:sometimesDoor");
178
+ return await ctx.db.insert("jobs", { namespace: args.namespace });
179
+ },
180
+ });
@@ -0,0 +1,104 @@
1
+ // CR-1 fixture — the RESOLVER and REFUSER helpers, shaped after vantage-peers
2
+ // 58ed649 `convex/lib/auth.ts` (withOrgScope, requireResolvedCaller,
3
+ // isNamespaceAllowedForScope, requireTenantNamespace, resolveOrgScopeForAction).
4
+ // Each helper below is a pole for tests/cr1-refusal-on-resolved.test.ts. Do not
5
+ // "tidy" it: a line that looks redundant is a discriminator.
6
+
7
+ /** RESOLVER (not a gate): an absent identity returns an anonymous scope. */
8
+ export async function withOrgScope(ctx: Ctx, opts?: Opts): Promise<Scope> {
9
+ const identity = await ctx.auth.getUserIdentity();
10
+ if (identity === null || identity === undefined) {
11
+ return { isMaster: false, orgSlug: null, anonymous: true, refused: true };
12
+ }
13
+ const row = await ctx.db
14
+ .query("client_org_mapping")
15
+ .withIndex("by_clerk_slug", (q) => q.eq("clerkSlug", identity.org_slug))
16
+ .unique();
17
+ if (row === null || opts?.refuseWithoutThrow) {
18
+ return { isMaster: false, orgSlug: null, anonymous: false, refused: true };
19
+ }
20
+ return {
21
+ isMaster: false,
22
+ orgSlug: row.orgSlug,
23
+ anonymous: false,
24
+ refused: false,
25
+ };
26
+ }
27
+
28
+ /** VALUE REFUSER, direct: refuses an anonymous scope by raising. */
29
+ export function requireResolvedCaller(
30
+ scope: Pick<Scope, "isMaster" | "orgSlug" | "anonymous" | "refused">,
31
+ registration: string,
32
+ opts?: { masterOnly?: boolean },
33
+ ): void {
34
+ if (scope.isMaster) return;
35
+ if (scope.anonymous) {
36
+ throw new ConvexError(
37
+ `RBAC_DENIED: no credential presented to "${registration}"`,
38
+ );
39
+ }
40
+ if (opts?.masterOnly) {
41
+ throw new ConvexError(
42
+ `RBAC_DENIED: "${registration}" admits the fleet master only`,
43
+ );
44
+ }
45
+ }
46
+
47
+ /** a PREDICATE on the resolved scope — returns a boolean, refuses nothing. */
48
+ export function isNamespaceAllowedForScope(
49
+ scope: Pick<Scope, "isMaster" | "orgSlug">,
50
+ namespace: string,
51
+ ): boolean {
52
+ if (scope.isMaster) return true;
53
+ if (scope.orgSlug === null) return false;
54
+ return namespace.startsWith(`team/${scope.orgSlug}`);
55
+ }
56
+
57
+ /** VALUE REFUSER, TRANSITIVE: no throw of its own — it allows on a test of the
58
+ * RESOLVED scope and otherwise hands that scope to requireResolvedCaller. */
59
+ export function requireTenantNamespace(
60
+ scope: Pick<Scope, "isMaster" | "orgSlug" | "anonymous" | "refused">,
61
+ namespace: string,
62
+ door: string,
63
+ ): void {
64
+ if (isNamespaceAllowedForScope(scope, namespace)) return;
65
+ requireResolvedCaller(scope, door, { masterOnly: true });
66
+ }
67
+
68
+ /** VALUE REFUSER, direct, one level: throws on a test of the resolved scope. */
69
+ export function requireMasterDirect(scope: Scope, door: string): void {
70
+ if (!scope.isMaster) {
71
+ throw new ConvexError(
72
+ `RBAC_DENIED: "${door}" admits the fleet master only`,
73
+ );
74
+ }
75
+ }
76
+
77
+ /** NOT a refuser of `scope`: it PROCEEDS on a test of a caller-supplied argument
78
+ * before any test of the resolved value — every principal, none included, whose
79
+ * namespace starts `public/` is waved through. */
80
+ export function requireNamespaceSometimes(
81
+ scope: Pick<Scope, "isMaster" | "orgSlug" | "anonymous" | "refused">,
82
+ namespace: string,
83
+ door: string,
84
+ ): void {
85
+ if (namespace.startsWith("public/")) return;
86
+ requireResolvedCaller(scope, door, { masterOnly: true });
87
+ }
88
+
89
+ /** a refuser of its OWN argument (a namespace), never of a principal. */
90
+ export function requireNonEmpty(namespace: string, door: string): void {
91
+ if (namespace.length === 0) {
92
+ throw new Error(`OKF_NAMESPACE_INVALID at "${door}"`);
93
+ }
94
+ }
95
+
96
+ /** the ONE way an action resolves its caller — reached as
97
+ * `internal.lib.auth.resolveOrgScopeForAction`, never by bare name. */
98
+ export const resolveOrgScopeForAction = internalQuery({
99
+ args: {},
100
+ handler: async (ctx) => {
101
+ const scope = await withOrgScope(ctx, { refuseWithoutThrow: true });
102
+ return scope;
103
+ },
104
+ });
@@ -0,0 +1,8 @@
1
+ // verified token here is forwarded verbatim to Convex as `clerkJwt` below,
2
+ // and Convex's own auth.config already requires aud === "convex", so binding
3
+ // the same value here (Critical Rule 14 element 5: aud + iss binding) closes
4
+ // the cross-tenant/cross-resource replay gap without rejecting any token a
5
+ // correctly-configured Clerk "convex" JWT template mints.
6
+ const CLERK_JWT_AUDIENCE = process.env.CLERK_JWT_AUDIENCE ?? "convex";
7
+
8
+ // Lazy singleton — createRemoteJWKSet caches JWKS in-process (10-min TTL).
@@ -0,0 +1,13 @@
1
+ * that DOES require the fallback).
2
+ */
3
+ async function tryVerifyClerkJwt(token: string): Promise<ClerkJwtResult> {
4
+ try {
5
+ const { payload } = await jwtVerify(token, clerkJwks(), {
6
+ issuer: CLERK_DOMAIN,
7
+ audience: CLERK_JWT_AUDIENCE,
8
+ });
9
+ // Verification succeeded — this IS the caller's own Clerk identity.
10
+ // `sub`/`exp` absence here would be a session-token shape violation
11
+ // (not the org concern this function types), so it is still
12
+ // "not-clerk" (try next layer) rather than "no-org".
13
+ const sub = payload.sub;
@@ -0,0 +1,55 @@
1
+ outil,table,crud,writer_tier,writer_gate,write_doors
2
+ import_okf_bundle,_storage+briefingNotes+memories+tasks,CREATE,org-member,orgResolver,okfBundleNode:importOkfBundle=orgResolver
3
+ store_document_chunked,_storage+kbUploads+memories,UPSERT,org-member,orgResolver,kb:storeDocumentChunked=orgResolver
4
+ delete_recurring_task,,DELETE,master,masterOnly,recurringTasks:remove=masterOnly
5
+ pause_recurring_task,,UPDATE,master,masterOnly,recurringTasks:pause=masterOnly
6
+ resume_recurring_task,,UPDATE,master,masterOnly,recurringTasks:resume=masterOnly
7
+ soft_delete_memory,,UPDATE,org-member,orgResolver,memories:softDeleteMemory=orgResolver
8
+ create_bu,businessUnits,CREATE,org-member,orgResolver,businessUnits:create=orgResolver
9
+ delete_bu,businessUnits,DELETE,master,masterOnly,businessUnits:remove=masterOnly
10
+ update_bu,businessUnits,UPDATE,org-member,orgResolver,businessUnits:update=orgResolver
11
+ update_task,client_org_mapping+messageReceipts+messages+missions+oauth_access_tokens+taskClosureConfig+tasks,UPSERT,org-member,orgResolver,tasks:update=orgResolver
12
+ send_message,client_org_mapping+messageReceipts+messages+profiles,CREATE,org-member,orgResolver,messages:sendMessage=orgResolver
13
+ update_recurring_task,client_org_mapping+oauth_access_tokens,UPDATE,org-member,orgResolver,recurringTasks:update=orgResolver
14
+ create_recurring_task,client_org_mapping+oauth_access_tokens+recurringTasks,CREATE,org-member,orgResolver,recurringTasks:create=orgResolver
15
+ create_task,client_org_mapping+oauth_access_tokens+tasks,CREATE,org-member,orgResolver,tasks:create=orgResolver
16
+ write_diary,diary,UPSERT,org-member,orgResolver,diary:write=orgResolver
17
+ add_fix_attempt,fixAttempts+fixPatterns,UPSERT,master,masterOnly,fixPatterns:addAttempt=masterOnly
18
+ create_fix_pattern,fixPatterns,CREATE,master,masterOnly,fixPatterns:create=masterOnly
19
+ link_issue_to_pattern,fixPatterns,UPDATE,master,masterOnly,fixPatterns:linkIssue=masterOnly
20
+ validate_fix,fixPatterns,UPDATE,master,masterOnly,fixPatterns:validate=masterOnly
21
+ complete_task,fixPatterns+githubRepoMapping+issues+messageReceipts+messages+taskClosureConfig+tasks,UPSERT,org-member,orgResolver,tasks:complete=orgResolver
22
+ add_repo_mapping,githubRepoMapping,UPSERT,master,masterOnly,githubRepoMapping:add=masterOnly
23
+ remove_repo_mapping,githubRepoMapping,DELETE,master,masterOnly,githubRepoMapping:remove=masterOnly
24
+ link_commit_to_issue,issues,UPDATE,master,masterOnly,issues:linkCommit=masterOnly
25
+ update_issue_status,issues,TRANSITION,master,masterOnly,issues:updateStatus=masterOnly
26
+ verify_issue,issues,TRANSITION,master,masterOnly,issues:verify=masterOnly
27
+ accept_mandate,mandates,TRANSITION,master,masterOnly,mandates:accept=masterOnly
28
+ create_mandate,mandates,CREATE,master,masterOnly,mandates:create=masterOnly
29
+ settle_mandate,mandates,TRANSITION,master,masterOnly,mandates:settle=masterOnly
30
+ update_mandate,mandates+tasks,UPDATE,master,masterOnly,mandates:update=masterOnly
31
+ soft_delete_document,memories,UPDATE,org-member,orgResolver,kb:softDeleteDocument=orgResolver
32
+ store_episode,memories,CREATE,org-member,orgResolver,episodes:storeEpisode=orgResolver
33
+ store_memory,memories,UPSERT,org-member,orgResolver,memories:storeMemory=orgResolver
34
+ mark_as_read,messageReceipts,UPDATE,org-member,orgResolver,messages:markAsRead=orgResolver
35
+ delete_message,messageReceipts+messages,DELETE,org-member,orgResolver,messages:deleteMessage=orgResolver
36
+ add_task_dependency,messageReceipts+messages+missions+taskClosureConfig+tasks,UPSERT,org-member,orgResolver,tasks:update=orgResolver
37
+ block_task,messageReceipts+messages+missions+taskClosureConfig+tasks,UPSERT,org-member,orgResolver,tasks:blockTask=orgResolver; tasks:update=orgResolver
38
+ fail_task,messageReceipts+messages+tasks,UPSERT,org-member,orgResolver,tasks:failTask=orgResolver
39
+ create_mission,missions,CREATE,org-member,orgResolver,missions:create=orgResolver
40
+ update_mission,missions,UPDATE,org-member,orgResolver,missions:update=orgResolver
41
+ update_mission_status,missions,TRANSITION,org-member,orgResolver,missions:updateStatus=orgResolver
42
+ soft_delete_mission_template,missionTemplates,UPDATE,master,masterOnly,missionTemplates:softDelete=masterOnly
43
+ update_mission_template,missionTemplates,UPSERT,master,masterOnly,missionTemplates:upsert=masterOnly
44
+ instantiate_template_into_mission,missionTemplates+missions+tasks,UPSERT,org-member,orgResolver,missionTemplates:instantiateTemplateIntoMission=orgResolver
45
+ add_deployment,monitoredDeployments,UPSERT,master,masterOnly,errorMonitor:addDeployment=masterOnly
46
+ remove_deployment,monitoredDeployments,UPDATE,master,masterOnly,errorMonitor:removeDeployment=masterOnly
47
+ set_summary,profiles,UPSERT,master,masterOnly,profiles:updateDynamic=masterOnly
48
+ update_profile,profiles,UPSERT,master,masterOnly,profiles:upsertProfile=masterOnly
49
+ bulk_complete_tasks,taskClosureConfig+tasks,TRANSITION,org-member,orgResolver,tasks:bulkComplete=orgResolver
50
+ correct_task_segment,taskClosureConfig+tasks,UPDATE,org-member,orgResolver,tasks:correctSegment=orgResolver
51
+ checkout_task,tasks,TRANSITION,org-member,orgResolver,tasks:checkout=orgResolver
52
+ delete_task,tasks,DELETE,org-member,orgResolver,tasks:deleteTask=orgResolver
53
+ pause_task,tasks,TRANSITION,org-member,orgResolver,tasks:pause=orgResolver
54
+ resume_task,tasks,TRANSITION,org-member,orgResolver,tasks:resume=orgResolver
55
+ start_task,tasks,UPDATE,org-member,orgResolver,tasks:start=orgResolver
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/backend-doctor",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "description": "A conformance instrument that measures the VantagePeers backend against the pinned backend standard. The THIRD doctor built ON the @vantageos/mcp-doctor engine — never a third engine. It consumes runDoctor/renderTable and adds only detectors.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "scripts": {
19
- "build": "tsc",
19
+ "build": "tsc && node scripts/stamp-build.mjs",
20
20
  "coverage": "tsx src/coverage-cli.ts",
21
21
  "doctor": "tsx src/cli.ts",
22
22
  "test": "vitest run",