@contractspec/lib.runtime-sandbox 2.7.5 → 2.7.9

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/README.md CHANGED
@@ -1,40 +1,60 @@
1
1
  # @contractspec/lib.runtime-sandbox
2
2
 
3
- Website: https://contractspec.io/
3
+ **Browser-compatible database abstraction built on PGLite for client-side SQL execution.**
4
4
 
5
- **Browser-friendly database abstraction with lazy-loaded PGLite.**
5
+ ## What It Provides
6
6
 
7
- Provides a `DatabasePort` interface and a PGLite adapter for running PostgreSQL-compatible queries in the browser. The adapter is lazy-loaded to avoid bundle bloat for consumers that don't need it.
7
+ - **Layer**: lib.
8
+ - **Consumers**: bundles, apps.
9
+ - `src/adapters/` contains runtime, provider, or environment-specific adapters.
10
+ - Related ContractSpec packages include `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
11
+ - `src/adapters/` contains runtime, provider, or environment-specific adapters.
8
12
 
9
13
  ## Installation
10
14
 
11
- ```bash
12
- bun add @contractspec/lib.runtime-sandbox
13
- ```
15
+ `npm install @contractspec/lib.runtime-sandbox`
14
16
 
15
- ## Exports
17
+ or
16
18
 
17
- - `.` -- `DatabasePort`, `DatabaseAdapterFactory`, `createPGLiteAdapter()`, `web` namespace, and core types
19
+ `bun add @contractspec/lib.runtime-sandbox`
18
20
 
19
21
  ## Usage
20
22
 
21
- ```ts
22
- import { createPGLiteAdapter } from "@contractspec/lib.runtime-sandbox";
23
+ Import the root entrypoint from `@contractspec/lib.runtime-sandbox`, or choose a documented subpath when you only need one part of the package surface.
23
24
 
24
- const db = await createPGLiteAdapter();
25
- await db.init();
25
+ ## Architecture
26
26
 
27
- await db.execute("CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)");
28
- await db.execute("INSERT INTO users VALUES ('1', 'Alice')");
27
+ - `src/adapters/` contains runtime, provider, or environment-specific adapters.
28
+ - `src/index.ts` is the root public barrel and package entrypoint.
29
+ - `src/ports` is part of the package's public or composition surface.
30
+ - `src/types` is part of the package's public or composition surface.
31
+ - `src/web` is part of the package's public or composition surface.
29
32
 
30
- const result = await db.query("SELECT * FROM users");
31
- console.log(result.rows);
32
- ```
33
+ ## Public Entry Points
33
34
 
34
- ### Web namespace
35
+ - Export `.` resolves through `./src/index.ts`.
35
36
 
36
- ```ts
37
- import { web } from "@contractspec/lib.runtime-sandbox";
38
- ```
37
+ ## Local Commands
39
38
 
40
- The `web` namespace provides browser-specific utilities for the sandbox runtime.
39
+ - `bun run dev` contractspec-bun-build dev
40
+ - `bun run build` — bun run prebuild && bun run build:bundle && bun run build:types
41
+ - `bun run lint` — bun run lint:fix
42
+ - `bun run lint:check` — biome check .
43
+ - `bun run lint:fix` — biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .
44
+ - `bun run typecheck` — tsc --noEmit
45
+ - `bun run publish:pkg` — bun publish --tolerate-republish --ignore-scripts --verbose
46
+ - `bun run publish:pkg:canary` — bun publish:pkg --tag canary
47
+ - `bun run clean` — rm -rf dist
48
+ - `bun run build:bundle` — contractspec-bun-build transpile
49
+ - `bun run build:types` — contractspec-bun-build types
50
+ - `bun run prebuild` — contractspec-bun-build prebuild
51
+
52
+ ## Recent Updates
53
+
54
+ - Replace eslint+prettier by biomejs to optimize speed.
55
+
56
+ ## Notes
57
+
58
+ - DatabasePort interface is the adapter boundary — consumers depend on the port, not the implementation.
59
+ - PGLite adapter must stay browser-compatible (no Node-only APIs).
60
+ - Migration schema must remain stable — breaking changes require a migration path.
@@ -228,16 +228,259 @@ async function seedWorkflowSystem(params) {
228
228
  const existing = await db.query(`SELECT COUNT(*) as count FROM workflow_definition WHERE "projectId" = $1`, [projectId]);
229
229
  if (existing.rows[0]?.count > 0)
230
230
  return;
231
- await db.execute(`INSERT INTO workflow_definition (id, "projectId", "organizationId", name, description, type, status)
232
- VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
233
- "wf_1",
234
- projectId,
235
- "org_demo",
236
- "Approval Workflow",
237
- "Demo approval workflow",
238
- "APPROVAL",
239
- "ACTIVE"
240
- ]);
231
+ const definitions = [
232
+ {
233
+ id: "wf_expense",
234
+ name: "Expense Approval",
235
+ description: "Approve non-trivial spend before finance releases budget.",
236
+ type: "APPROVAL",
237
+ status: "ACTIVE",
238
+ createdAt: "2026-03-10T09:00:00.000Z",
239
+ updatedAt: "2026-03-20T08:15:00.000Z",
240
+ steps: [
241
+ {
242
+ id: "wfstep_expense_manager",
243
+ name: "Manager Review",
244
+ description: "Validate business value and team budget.",
245
+ stepOrder: 1,
246
+ type: "APPROVAL",
247
+ requiredRoles: ["manager"],
248
+ createdAt: "2026-03-10T09:00:00.000Z"
249
+ },
250
+ {
251
+ id: "wfstep_expense_finance",
252
+ name: "Finance Review",
253
+ description: "Confirm ledger coding and spending threshold.",
254
+ stepOrder: 2,
255
+ type: "APPROVAL",
256
+ requiredRoles: ["finance"],
257
+ createdAt: "2026-03-10T09:10:00.000Z"
258
+ }
259
+ ]
260
+ },
261
+ {
262
+ id: "wf_vendor",
263
+ name: "Vendor Onboarding",
264
+ description: "Sequence security, procurement, and legal before activation.",
265
+ type: "SEQUENTIAL",
266
+ status: "ACTIVE",
267
+ createdAt: "2026-03-08T11:00:00.000Z",
268
+ updatedAt: "2026-03-19T13:10:00.000Z",
269
+ steps: [
270
+ {
271
+ id: "wfstep_vendor_security",
272
+ name: "Security Check",
273
+ description: "Review data access and integration scope.",
274
+ stepOrder: 1,
275
+ type: "APPROVAL",
276
+ requiredRoles: ["security"],
277
+ createdAt: "2026-03-08T11:00:00.000Z"
278
+ },
279
+ {
280
+ id: "wfstep_vendor_procurement",
281
+ name: "Procurement Check",
282
+ description: "Validate pricing, procurement policy, and owner.",
283
+ stepOrder: 2,
284
+ type: "APPROVAL",
285
+ requiredRoles: ["procurement"],
286
+ createdAt: "2026-03-08T11:05:00.000Z"
287
+ },
288
+ {
289
+ id: "wfstep_vendor_legal",
290
+ name: "Legal Sign-off",
291
+ description: "Approve terms before the vendor goes live.",
292
+ stepOrder: 3,
293
+ type: "APPROVAL",
294
+ requiredRoles: ["legal"],
295
+ createdAt: "2026-03-08T11:10:00.000Z"
296
+ }
297
+ ]
298
+ },
299
+ {
300
+ id: "wf_policy_exception",
301
+ name: "Policy Exception",
302
+ description: "Escalate a temporary exception through team lead and compliance.",
303
+ type: "APPROVAL",
304
+ status: "DRAFT",
305
+ createdAt: "2026-03-15T07:30:00.000Z",
306
+ updatedAt: "2026-03-18T11:20:00.000Z",
307
+ steps: [
308
+ {
309
+ id: "wfstep_policy_lead",
310
+ name: "Team Lead Review",
311
+ description: "Check urgency and expected blast radius.",
312
+ stepOrder: 1,
313
+ type: "APPROVAL",
314
+ requiredRoles: ["team-lead"],
315
+ createdAt: "2026-03-15T07:30:00.000Z"
316
+ },
317
+ {
318
+ id: "wfstep_policy_compliance",
319
+ name: "Compliance Review",
320
+ description: "Accept or reject the exception request.",
321
+ stepOrder: 2,
322
+ type: "APPROVAL",
323
+ requiredRoles: ["compliance"],
324
+ createdAt: "2026-03-15T07:40:00.000Z"
325
+ }
326
+ ]
327
+ }
328
+ ];
329
+ const instances = [
330
+ {
331
+ id: "wfinst_expense_open",
332
+ definitionId: "wf_expense",
333
+ status: "IN_PROGRESS",
334
+ currentStepId: "wfstep_expense_finance",
335
+ data: { amount: 4200, currency: "EUR", vendor: "Nimbus AI" },
336
+ requestedBy: "sarah@contractspec.io",
337
+ startedAt: "2026-03-20T08:00:00.000Z",
338
+ completedAt: null,
339
+ approvals: [
340
+ {
341
+ id: "wfappr_expense_manager",
342
+ stepId: "wfstep_expense_manager",
343
+ status: "APPROVED",
344
+ actorId: "manager.demo",
345
+ comment: "Approved for the Q2 automation budget.",
346
+ decidedAt: "2026-03-20T08:15:00.000Z",
347
+ createdAt: "2026-03-20T08:05:00.000Z"
348
+ },
349
+ {
350
+ id: "wfappr_expense_finance",
351
+ stepId: "wfstep_expense_finance",
352
+ status: "PENDING",
353
+ actorId: null,
354
+ comment: null,
355
+ decidedAt: null,
356
+ createdAt: "2026-03-20T08:15:00.000Z"
357
+ }
358
+ ]
359
+ },
360
+ {
361
+ id: "wfinst_vendor_done",
362
+ definitionId: "wf_vendor",
363
+ status: "COMPLETED",
364
+ currentStepId: null,
365
+ data: { vendor: "Acme Cloud", riskTier: "medium" },
366
+ requestedBy: "leo@contractspec.io",
367
+ startedAt: "2026-03-19T09:30:00.000Z",
368
+ completedAt: "2026-03-19T13:10:00.000Z",
369
+ approvals: [
370
+ {
371
+ id: "wfappr_vendor_security",
372
+ stepId: "wfstep_vendor_security",
373
+ status: "APPROVED",
374
+ actorId: "security.demo",
375
+ comment: "SOC2 scope is acceptable.",
376
+ decidedAt: "2026-03-19T10:10:00.000Z",
377
+ createdAt: "2026-03-19T09:35:00.000Z"
378
+ },
379
+ {
380
+ id: "wfappr_vendor_procurement",
381
+ stepId: "wfstep_vendor_procurement",
382
+ status: "APPROVED",
383
+ actorId: "procurement.demo",
384
+ comment: "Commercial terms match the preferred vendor tier.",
385
+ decidedAt: "2026-03-19T11:25:00.000Z",
386
+ createdAt: "2026-03-19T10:15:00.000Z"
387
+ },
388
+ {
389
+ id: "wfappr_vendor_legal",
390
+ stepId: "wfstep_vendor_legal",
391
+ status: "APPROVED",
392
+ actorId: "legal.demo",
393
+ comment: "MSA redlines are complete.",
394
+ decidedAt: "2026-03-19T13:05:00.000Z",
395
+ createdAt: "2026-03-19T11:30:00.000Z"
396
+ }
397
+ ]
398
+ },
399
+ {
400
+ id: "wfinst_policy_rejected",
401
+ definitionId: "wf_policy_exception",
402
+ status: "REJECTED",
403
+ currentStepId: "wfstep_policy_compliance",
404
+ data: { policy: "Model rollout freeze", durationDays: 14 },
405
+ requestedBy: "maya@contractspec.io",
406
+ startedAt: "2026-03-18T10:00:00.000Z",
407
+ completedAt: "2026-03-18T11:20:00.000Z",
408
+ approvals: [
409
+ {
410
+ id: "wfappr_policy_lead",
411
+ stepId: "wfstep_policy_lead",
412
+ status: "APPROVED",
413
+ actorId: "lead.demo",
414
+ comment: "Escalation justified for the release train.",
415
+ decidedAt: "2026-03-18T10:30:00.000Z",
416
+ createdAt: "2026-03-18T10:05:00.000Z"
417
+ },
418
+ {
419
+ id: "wfappr_policy_compliance",
420
+ stepId: "wfstep_policy_compliance",
421
+ status: "REJECTED",
422
+ actorId: "compliance.demo",
423
+ comment: "Exception exceeds the allowed blast radius.",
424
+ decidedAt: "2026-03-18T11:15:00.000Z",
425
+ createdAt: "2026-03-18T10:35:00.000Z"
426
+ }
427
+ ]
428
+ }
429
+ ];
430
+ for (const definition of definitions) {
431
+ await db.execute(`INSERT INTO workflow_definition (id, "projectId", "organizationId", name, description, type, status, "createdAt", "updatedAt")
432
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
433
+ definition.id,
434
+ projectId,
435
+ "org_demo",
436
+ definition.name,
437
+ definition.description,
438
+ definition.type,
439
+ definition.status,
440
+ definition.createdAt,
441
+ definition.updatedAt
442
+ ]);
443
+ for (const step of definition.steps) {
444
+ await db.execute(`INSERT INTO workflow_step (id, "definitionId", name, description, "stepOrder", type, "requiredRoles", "createdAt")
445
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [
446
+ step.id,
447
+ definition.id,
448
+ step.name,
449
+ step.description,
450
+ step.stepOrder,
451
+ step.type,
452
+ JSON.stringify(step.requiredRoles),
453
+ step.createdAt
454
+ ]);
455
+ }
456
+ }
457
+ for (const instance of instances) {
458
+ await db.execute(`INSERT INTO workflow_instance (id, "projectId", "definitionId", status, "currentStepId", data, "requestedBy", "startedAt", "completedAt")
459
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
460
+ instance.id,
461
+ projectId,
462
+ instance.definitionId,
463
+ instance.status,
464
+ instance.currentStepId,
465
+ JSON.stringify(instance.data),
466
+ instance.requestedBy,
467
+ instance.startedAt,
468
+ instance.completedAt
469
+ ]);
470
+ for (const approval of instance.approvals) {
471
+ await db.execute(`INSERT INTO workflow_approval (id, "instanceId", "stepId", status, "actorId", comment, "decidedAt", "createdAt")
472
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [
473
+ approval.id,
474
+ instance.id,
475
+ approval.stepId,
476
+ approval.status,
477
+ approval.actorId,
478
+ approval.comment,
479
+ approval.decidedAt,
480
+ approval.createdAt
481
+ ]);
482
+ }
483
+ }
241
484
  }
242
485
  async function seedMarketplace(params) {
243
486
  const { projectId, db } = params;
@@ -1702,116 +1945,6 @@ var psaReviewTask = pgTable("psa_review_task", {
1702
1945
  decidedAt: text("decidedAt"),
1703
1946
  decidedBy: text("decidedBy")
1704
1947
  });
1705
- // src/web/storage/indexeddb.ts
1706
- var DEFAULT_DB_NAME = "contractspec-runtime";
1707
- var DEFAULT_STORE = "kv";
1708
- var FALLBACK_STORE = new Map;
1709
-
1710
- class LocalStorageService {
1711
- options;
1712
- dbPromise;
1713
- constructor(options = {}) {
1714
- this.options = options;
1715
- }
1716
- async init() {
1717
- await this.getDb();
1718
- }
1719
- async get(key, fallback) {
1720
- const db = await this.getDb();
1721
- if (!db) {
1722
- return FALLBACK_STORE.get(key) ?? fallback;
1723
- }
1724
- return new Promise((resolve, reject) => {
1725
- const tx = db.transaction(this.storeName, "readonly");
1726
- const store = tx.objectStore(this.storeName);
1727
- const request = store.get(key);
1728
- request.onsuccess = () => {
1729
- resolve(request.result ?? fallback);
1730
- };
1731
- request.onerror = () => reject(request.error);
1732
- });
1733
- }
1734
- async set(key, value) {
1735
- const db = await this.getDb();
1736
- if (!db) {
1737
- FALLBACK_STORE.set(key, value);
1738
- return;
1739
- }
1740
- await new Promise((resolve, reject) => {
1741
- const tx = db.transaction(this.storeName, "readwrite");
1742
- const store = tx.objectStore(this.storeName);
1743
- const request = store.put(value, key);
1744
- request.onsuccess = () => resolve();
1745
- request.onerror = () => reject(request.error);
1746
- });
1747
- }
1748
- async delete(key) {
1749
- const db = await this.getDb();
1750
- if (!db) {
1751
- FALLBACK_STORE.delete(key);
1752
- return;
1753
- }
1754
- await new Promise((resolve, reject) => {
1755
- const tx = db.transaction(this.storeName, "readwrite");
1756
- const store = tx.objectStore(this.storeName);
1757
- const request = store.delete(key);
1758
- request.onsuccess = () => resolve();
1759
- request.onerror = () => reject(request.error);
1760
- });
1761
- }
1762
- async clear() {
1763
- const db = await this.getDb();
1764
- if (!db) {
1765
- FALLBACK_STORE.clear();
1766
- return;
1767
- }
1768
- await new Promise((resolve, reject) => {
1769
- const tx = db.transaction(this.storeName, "readwrite");
1770
- const store = tx.objectStore(this.storeName);
1771
- const request = store.clear();
1772
- request.onsuccess = () => resolve();
1773
- request.onerror = () => reject(request.error);
1774
- });
1775
- }
1776
- get storeName() {
1777
- return this.options.storeName ?? DEFAULT_STORE;
1778
- }
1779
- async getDb() {
1780
- if (typeof indexedDB === "undefined") {
1781
- return null;
1782
- }
1783
- if (!this.dbPromise) {
1784
- this.dbPromise = this.openDb();
1785
- }
1786
- return this.dbPromise;
1787
- }
1788
- openDb() {
1789
- return new Promise((resolve, reject) => {
1790
- const request = indexedDB.open(this.options.dbName ?? DEFAULT_DB_NAME, this.options.version ?? 1);
1791
- request.onerror = () => reject(request.error);
1792
- request.onsuccess = () => {
1793
- resolve(request.result);
1794
- };
1795
- request.onupgradeneeded = () => {
1796
- const db = request.result;
1797
- if (!db.objectStoreNames.contains(this.storeName)) {
1798
- db.createObjectStore(this.storeName);
1799
- }
1800
- };
1801
- });
1802
- }
1803
- }
1804
- // src/web/utils/id.ts
1805
- function generateId(prefix) {
1806
- const base = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2, 10);
1807
- return prefix ? `${prefix}_${base}` : base;
1808
- }
1809
- // src/web/graphql/local-client.ts
1810
- import { ApolloClient, InMemoryCache } from "@apollo/client";
1811
- import { SchemaLink } from "@apollo/client/link/schema";
1812
- import { makeExecutableSchema } from "@graphql-tools/schema";
1813
- import { GraphQLScalarType, Kind } from "graphql";
1814
-
1815
1948
  // src/web/events/local-pubsub.ts
1816
1949
  class LocalEventBus {
1817
1950
  listeners = new Map;
@@ -1835,6 +1968,17 @@ class LocalEventBus {
1835
1968
  };
1836
1969
  }
1837
1970
  }
1971
+ // src/web/graphql/local-client.ts
1972
+ import { ApolloClient, InMemoryCache } from "@apollo/client";
1973
+ import { SchemaLink } from "@apollo/client/link/schema";
1974
+ import { makeExecutableSchema } from "@graphql-tools/schema";
1975
+ import { GraphQLScalarType, Kind } from "graphql";
1976
+
1977
+ // src/web/utils/id.ts
1978
+ function generateId(prefix) {
1979
+ const base = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2, 10);
1980
+ return prefix ? `${prefix}_${base}` : base;
1981
+ }
1838
1982
 
1839
1983
  // src/web/graphql/local-client.ts
1840
1984
  var typeDefs = `
@@ -2381,6 +2525,106 @@ function mapRecipeInstruction(row, locale) {
2381
2525
  ordering: row.ordering ?? 0
2382
2526
  };
2383
2527
  }
2528
+ // src/web/storage/indexeddb.ts
2529
+ var DEFAULT_DB_NAME = "contractspec-runtime";
2530
+ var DEFAULT_STORE = "kv";
2531
+ var FALLBACK_STORE = new Map;
2532
+
2533
+ class LocalStorageService {
2534
+ options;
2535
+ dbPromise;
2536
+ constructor(options = {}) {
2537
+ this.options = options;
2538
+ }
2539
+ async init() {
2540
+ await this.getDb();
2541
+ }
2542
+ async get(key, fallback) {
2543
+ const db = await this.getDb();
2544
+ if (!db) {
2545
+ return FALLBACK_STORE.get(key) ?? fallback;
2546
+ }
2547
+ return new Promise((resolve, reject) => {
2548
+ const tx = db.transaction(this.storeName, "readonly");
2549
+ const store = tx.objectStore(this.storeName);
2550
+ const request = store.get(key);
2551
+ request.onsuccess = () => {
2552
+ resolve(request.result ?? fallback);
2553
+ };
2554
+ request.onerror = () => reject(request.error);
2555
+ });
2556
+ }
2557
+ async set(key, value) {
2558
+ const db = await this.getDb();
2559
+ if (!db) {
2560
+ FALLBACK_STORE.set(key, value);
2561
+ return;
2562
+ }
2563
+ await new Promise((resolve, reject) => {
2564
+ const tx = db.transaction(this.storeName, "readwrite");
2565
+ const store = tx.objectStore(this.storeName);
2566
+ const request = store.put(value, key);
2567
+ request.onsuccess = () => resolve();
2568
+ request.onerror = () => reject(request.error);
2569
+ });
2570
+ }
2571
+ async delete(key) {
2572
+ const db = await this.getDb();
2573
+ if (!db) {
2574
+ FALLBACK_STORE.delete(key);
2575
+ return;
2576
+ }
2577
+ await new Promise((resolve, reject) => {
2578
+ const tx = db.transaction(this.storeName, "readwrite");
2579
+ const store = tx.objectStore(this.storeName);
2580
+ const request = store.delete(key);
2581
+ request.onsuccess = () => resolve();
2582
+ request.onerror = () => reject(request.error);
2583
+ });
2584
+ }
2585
+ async clear() {
2586
+ const db = await this.getDb();
2587
+ if (!db) {
2588
+ FALLBACK_STORE.clear();
2589
+ return;
2590
+ }
2591
+ await new Promise((resolve, reject) => {
2592
+ const tx = db.transaction(this.storeName, "readwrite");
2593
+ const store = tx.objectStore(this.storeName);
2594
+ const request = store.clear();
2595
+ request.onsuccess = () => resolve();
2596
+ request.onerror = () => reject(request.error);
2597
+ });
2598
+ }
2599
+ get storeName() {
2600
+ return this.options.storeName ?? DEFAULT_STORE;
2601
+ }
2602
+ async getDb() {
2603
+ if (typeof indexedDB === "undefined") {
2604
+ return null;
2605
+ }
2606
+ if (!this.dbPromise) {
2607
+ this.dbPromise = this.openDb();
2608
+ }
2609
+ return this.dbPromise;
2610
+ }
2611
+ openDb() {
2612
+ return new Promise((resolve, reject) => {
2613
+ const request = indexedDB.open(this.options.dbName ?? DEFAULT_DB_NAME, this.options.version ?? 1);
2614
+ request.onerror = () => reject(request.error);
2615
+ request.onsuccess = () => {
2616
+ resolve(request.result);
2617
+ };
2618
+ request.onupgradeneeded = () => {
2619
+ const db = request.result;
2620
+ if (!db.objectStoreNames.contains(this.storeName)) {
2621
+ db.createObjectStore(this.storeName);
2622
+ }
2623
+ };
2624
+ });
2625
+ }
2626
+ }
2627
+
2384
2628
  // src/web/runtime/services.ts
2385
2629
  var DEFAULT_PROJECT_ID = "local-project";
2386
2630