lula2 0.1.1-nightly.0 → 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.
@@ -2810,6 +2810,7 @@ __export(spreadsheetRoutes_exports, {
2810
2810
  default: () => spreadsheetRoutes_default,
2811
2811
  scanControlSets: () => scanControlSets
2812
2812
  });
2813
+ import crypto from "crypto";
2813
2814
  import { parse as parseCSVSync } from "csv-parse/sync";
2814
2815
  import ExcelJS from "exceljs";
2815
2816
  import express from "express";
@@ -3128,6 +3129,15 @@ var init_spreadsheetRoutes = __esm({
3128
3129
  controlSetName = "Imported Control Set",
3129
3130
  controlSetDescription = "Imported from spreadsheet"
3130
3131
  } = req.body;
3132
+ let justificationFields = [];
3133
+ if (req.body.justificationFields) {
3134
+ try {
3135
+ justificationFields = JSON.parse(req.body.justificationFields);
3136
+ debug("Justification fields received:", justificationFields);
3137
+ } catch (e) {
3138
+ console.error("Failed to parse justification fields:", e);
3139
+ }
3140
+ }
3131
3141
  debug("Import parameters received:", {
3132
3142
  controlIdField,
3133
3143
  startRow,
@@ -3375,11 +3385,17 @@ var init_spreadsheetRoutes = __esm({
3375
3385
  };
3376
3386
  writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
3377
3387
  const controlsDir = join4(baseDir, "controls");
3388
+ const mappingsDir = join4(baseDir, "mappings");
3389
+ const justificationFieldNames = justificationFields;
3378
3390
  families.forEach((familyControls, family) => {
3379
3391
  const familyDir = join4(controlsDir, family);
3392
+ const familyMappingsDir = join4(mappingsDir, family);
3380
3393
  if (!existsSync3(familyDir)) {
3381
3394
  mkdirSync2(familyDir, { recursive: true });
3382
3395
  }
3396
+ if (!existsSync3(familyMappingsDir)) {
3397
+ mkdirSync2(familyMappingsDir, { recursive: true });
3398
+ }
3383
3399
  familyControls.forEach((control) => {
3384
3400
  const controlId = control[controlIdFieldNameClean];
3385
3401
  if (!controlId) {
@@ -3389,12 +3405,24 @@ var init_spreadsheetRoutes = __esm({
3389
3405
  const controlIdStr = String(controlId).slice(0, 50);
3390
3406
  const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
3391
3407
  const filePath = join4(familyDir, fileName2);
3408
+ const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
3409
+ const mappingFilePath = join4(familyMappingsDir, mappingFileName);
3392
3410
  const filteredControl = {};
3411
+ const mappingData = {
3412
+ control_id: controlIdStr,
3413
+ justification: "",
3414
+ uuid: crypto.randomUUID()
3415
+ };
3416
+ const justificationContents = [];
3393
3417
  if (control.family !== void 0) {
3394
3418
  filteredControl.family = control.family;
3395
3419
  }
3396
3420
  Object.keys(control).forEach((fieldName) => {
3397
3421
  if (fieldName === "family") return;
3422
+ if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
3423
+ justificationContents.push(control[fieldName]);
3424
+ return;
3425
+ }
3398
3426
  const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
3399
3427
  const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
3400
3428
  if (isInFrontendSchema || isInFieldsMetadata) {
@@ -3402,6 +3430,13 @@ var init_spreadsheetRoutes = __esm({
3402
3430
  }
3403
3431
  });
3404
3432
  writeFileSync2(filePath, yaml4.dump(filteredControl));
3433
+ if (justificationContents.length > 0) {
3434
+ mappingData.justification = justificationContents.join("\n\n");
3435
+ }
3436
+ if (mappingData.justification && mappingData.justification.trim() !== "") {
3437
+ const mappingArray = [mappingData];
3438
+ writeFileSync2(mappingFilePath, yaml4.dump(mappingArray));
3439
+ }
3405
3440
  });
3406
3441
  });
3407
3442
  res.json({
@@ -4510,8 +4545,8 @@ var WebSocketManager = class {
4510
4545
  if (payload && payload.control_id) {
4511
4546
  const mapping = payload;
4512
4547
  if (!mapping.uuid) {
4513
- const crypto = await import("crypto");
4514
- mapping.uuid = crypto.randomUUID();
4548
+ const crypto2 = await import("crypto");
4549
+ mapping.uuid = crypto2.randomUUID();
4515
4550
  }
4516
4551
  await state.fileStore.saveMapping(mapping);
4517
4552
  state.mappingsCache.set(mapping.uuid, mapping);
@@ -1,4 +1,5 @@
1
1
  // cli/server/spreadsheetRoutes.ts
2
+ import crypto from "crypto";
2
3
  import { parse as parseCSVSync } from "csv-parse/sync";
3
4
  import ExcelJS from "exceljs";
4
5
  import express from "express";
@@ -93,6 +94,15 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
93
94
  controlSetName = "Imported Control Set",
94
95
  controlSetDescription = "Imported from spreadsheet"
95
96
  } = req.body;
97
+ let justificationFields = [];
98
+ if (req.body.justificationFields) {
99
+ try {
100
+ justificationFields = JSON.parse(req.body.justificationFields);
101
+ debug("Justification fields received:", justificationFields);
102
+ } catch (e) {
103
+ console.error("Failed to parse justification fields:", e);
104
+ }
105
+ }
96
106
  debug("Import parameters received:", {
97
107
  controlIdField,
98
108
  startRow,
@@ -340,11 +350,17 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
340
350
  };
341
351
  writeFileSync(join(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
342
352
  const controlsDir = join(baseDir, "controls");
353
+ const mappingsDir = join(baseDir, "mappings");
354
+ const justificationFieldNames = justificationFields;
343
355
  families.forEach((familyControls, family) => {
344
356
  const familyDir = join(controlsDir, family);
357
+ const familyMappingsDir = join(mappingsDir, family);
345
358
  if (!existsSync(familyDir)) {
346
359
  mkdirSync(familyDir, { recursive: true });
347
360
  }
361
+ if (!existsSync(familyMappingsDir)) {
362
+ mkdirSync(familyMappingsDir, { recursive: true });
363
+ }
348
364
  familyControls.forEach((control) => {
349
365
  const controlId = control[controlIdFieldNameClean];
350
366
  if (!controlId) {
@@ -354,12 +370,24 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
354
370
  const controlIdStr = String(controlId).slice(0, 50);
355
371
  const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
356
372
  const filePath = join(familyDir, fileName2);
373
+ const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
374
+ const mappingFilePath = join(familyMappingsDir, mappingFileName);
357
375
  const filteredControl = {};
376
+ const mappingData = {
377
+ control_id: controlIdStr,
378
+ justification: "",
379
+ uuid: crypto.randomUUID()
380
+ };
381
+ const justificationContents = [];
358
382
  if (control.family !== void 0) {
359
383
  filteredControl.family = control.family;
360
384
  }
361
385
  Object.keys(control).forEach((fieldName) => {
362
386
  if (fieldName === "family") return;
387
+ if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
388
+ justificationContents.push(control[fieldName]);
389
+ return;
390
+ }
363
391
  const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
364
392
  const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
365
393
  if (isInFrontendSchema || isInFieldsMetadata) {
@@ -367,6 +395,13 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
367
395
  }
368
396
  });
369
397
  writeFileSync(filePath, yaml4.dump(filteredControl));
398
+ if (justificationContents.length > 0) {
399
+ mappingData.justification = justificationContents.join("\n\n");
400
+ }
401
+ if (mappingData.justification && mappingData.justification.trim() !== "") {
402
+ const mappingArray = [mappingData];
403
+ writeFileSync(mappingFilePath, yaml4.dump(mappingArray));
404
+ }
370
405
  });
371
406
  });
372
407
  res.json({
@@ -1257,6 +1257,7 @@ __export(spreadsheetRoutes_exports, {
1257
1257
  default: () => spreadsheetRoutes_default,
1258
1258
  scanControlSets: () => scanControlSets
1259
1259
  });
1260
+ import crypto from "crypto";
1260
1261
  import { parse as parseCSVSync } from "csv-parse/sync";
1261
1262
  import ExcelJS from "exceljs";
1262
1263
  import express from "express";
@@ -1575,6 +1576,15 @@ var init_spreadsheetRoutes = __esm({
1575
1576
  controlSetName = "Imported Control Set",
1576
1577
  controlSetDescription = "Imported from spreadsheet"
1577
1578
  } = req.body;
1579
+ let justificationFields = [];
1580
+ if (req.body.justificationFields) {
1581
+ try {
1582
+ justificationFields = JSON.parse(req.body.justificationFields);
1583
+ debug("Justification fields received:", justificationFields);
1584
+ } catch (e) {
1585
+ console.error("Failed to parse justification fields:", e);
1586
+ }
1587
+ }
1578
1588
  debug("Import parameters received:", {
1579
1589
  controlIdField,
1580
1590
  startRow,
@@ -1822,11 +1832,17 @@ var init_spreadsheetRoutes = __esm({
1822
1832
  };
1823
1833
  writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
1824
1834
  const controlsDir = join4(baseDir, "controls");
1835
+ const mappingsDir = join4(baseDir, "mappings");
1836
+ const justificationFieldNames = justificationFields;
1825
1837
  families.forEach((familyControls, family) => {
1826
1838
  const familyDir = join4(controlsDir, family);
1839
+ const familyMappingsDir = join4(mappingsDir, family);
1827
1840
  if (!existsSync3(familyDir)) {
1828
1841
  mkdirSync2(familyDir, { recursive: true });
1829
1842
  }
1843
+ if (!existsSync3(familyMappingsDir)) {
1844
+ mkdirSync2(familyMappingsDir, { recursive: true });
1845
+ }
1830
1846
  familyControls.forEach((control) => {
1831
1847
  const controlId = control[controlIdFieldNameClean];
1832
1848
  if (!controlId) {
@@ -1836,12 +1852,24 @@ var init_spreadsheetRoutes = __esm({
1836
1852
  const controlIdStr = String(controlId).slice(0, 50);
1837
1853
  const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
1838
1854
  const filePath = join4(familyDir, fileName2);
1855
+ const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
1856
+ const mappingFilePath = join4(familyMappingsDir, mappingFileName);
1839
1857
  const filteredControl = {};
1858
+ const mappingData = {
1859
+ control_id: controlIdStr,
1860
+ justification: "",
1861
+ uuid: crypto.randomUUID()
1862
+ };
1863
+ const justificationContents = [];
1840
1864
  if (control.family !== void 0) {
1841
1865
  filteredControl.family = control.family;
1842
1866
  }
1843
1867
  Object.keys(control).forEach((fieldName) => {
1844
1868
  if (fieldName === "family") return;
1869
+ if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
1870
+ justificationContents.push(control[fieldName]);
1871
+ return;
1872
+ }
1845
1873
  const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
1846
1874
  const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
1847
1875
  if (isInFrontendSchema || isInFieldsMetadata) {
@@ -1849,6 +1877,13 @@ var init_spreadsheetRoutes = __esm({
1849
1877
  }
1850
1878
  });
1851
1879
  writeFileSync2(filePath, yaml4.dump(filteredControl));
1880
+ if (justificationContents.length > 0) {
1881
+ mappingData.justification = justificationContents.join("\n\n");
1882
+ }
1883
+ if (mappingData.justification && mappingData.justification.trim() !== "") {
1884
+ const mappingArray = [mappingData];
1885
+ writeFileSync2(mappingFilePath, yaml4.dump(mappingArray));
1886
+ }
1852
1887
  });
1853
1888
  });
1854
1889
  res.json({
@@ -2090,8 +2125,8 @@ var WebSocketManager = class {
2090
2125
  if (payload && payload.control_id) {
2091
2126
  const mapping = payload;
2092
2127
  if (!mapping.uuid) {
2093
- const crypto = await import("crypto");
2094
- mapping.uuid = crypto.randomUUID();
2128
+ const crypto2 = await import("crypto");
2129
+ mapping.uuid = crypto2.randomUUID();
2095
2130
  }
2096
2131
  await state.fileStore.saveMapping(mapping);
2097
2132
  state.mappingsCache.set(mapping.uuid, mapping);
package/dist/index.html CHANGED
@@ -6,10 +6,10 @@
6
6
  <link rel="icon" href="/lula.png" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
8
 
9
- <link rel="modulepreload" href="/_app/immutable/entry/start.Cdw5p6kF.js">
10
- <link rel="modulepreload" href="/_app/immutable/chunks/D3AAXov_.js">
9
+ <link rel="modulepreload" href="/_app/immutable/entry/start.z83Vr3Ay.js">
10
+ <link rel="modulepreload" href="/_app/immutable/chunks/Dacgmabg.js">
11
11
  <link rel="modulepreload" href="/_app/immutable/chunks/Cby0Z7eP.js">
12
- <link rel="modulepreload" href="/_app/immutable/entry/app.pY_jLQAk.js">
12
+ <link rel="modulepreload" href="/_app/immutable/entry/app.lS9kJbUY.js">
13
13
  <link rel="modulepreload" href="/_app/immutable/chunks/DsnmJJEf.js">
14
14
  <link rel="modulepreload" href="/_app/immutable/chunks/DqsOU3kV.js">
15
15
  <link rel="modulepreload" href="/_app/immutable/chunks/CoF2vljD.js">
@@ -19,15 +19,15 @@
19
19
  <div style="display: contents">
20
20
  <script>
21
21
  {
22
- __sveltekit_rtxdy2 = {
22
+ __sveltekit_1lw4tcu = {
23
23
  base: ""
24
24
  };
25
25
 
26
26
  const element = document.currentScript.parentElement;
27
27
 
28
28
  Promise.all([
29
- import("/_app/immutable/entry/start.Cdw5p6kF.js"),
30
- import("/_app/immutable/entry/app.pY_jLQAk.js")
29
+ import("/_app/immutable/entry/start.z83Vr3Ay.js"),
30
+ import("/_app/immutable/entry/app.lS9kJbUY.js")
31
31
  ]).then(([kit, app]) => {
32
32
  kit.start(app, element);
33
33
  });
package/dist/index.js CHANGED
@@ -2829,6 +2829,7 @@ __export(spreadsheetRoutes_exports, {
2829
2829
  default: () => spreadsheetRoutes_default,
2830
2830
  scanControlSets: () => scanControlSets
2831
2831
  });
2832
+ import crypto from "crypto";
2832
2833
  import { parse as parseCSVSync } from "csv-parse/sync";
2833
2834
  import ExcelJS from "exceljs";
2834
2835
  import express from "express";
@@ -3147,6 +3148,15 @@ var init_spreadsheetRoutes = __esm({
3147
3148
  controlSetName = "Imported Control Set",
3148
3149
  controlSetDescription = "Imported from spreadsheet"
3149
3150
  } = req.body;
3151
+ let justificationFields = [];
3152
+ if (req.body.justificationFields) {
3153
+ try {
3154
+ justificationFields = JSON.parse(req.body.justificationFields);
3155
+ debug("Justification fields received:", justificationFields);
3156
+ } catch (e) {
3157
+ console.error("Failed to parse justification fields:", e);
3158
+ }
3159
+ }
3150
3160
  debug("Import parameters received:", {
3151
3161
  controlIdField,
3152
3162
  startRow,
@@ -3394,11 +3404,17 @@ var init_spreadsheetRoutes = __esm({
3394
3404
  };
3395
3405
  writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
3396
3406
  const controlsDir = join4(baseDir, "controls");
3407
+ const mappingsDir = join4(baseDir, "mappings");
3408
+ const justificationFieldNames = justificationFields;
3397
3409
  families.forEach((familyControls, family) => {
3398
3410
  const familyDir = join4(controlsDir, family);
3411
+ const familyMappingsDir = join4(mappingsDir, family);
3399
3412
  if (!existsSync3(familyDir)) {
3400
3413
  mkdirSync2(familyDir, { recursive: true });
3401
3414
  }
3415
+ if (!existsSync3(familyMappingsDir)) {
3416
+ mkdirSync2(familyMappingsDir, { recursive: true });
3417
+ }
3402
3418
  familyControls.forEach((control) => {
3403
3419
  const controlId = control[controlIdFieldNameClean];
3404
3420
  if (!controlId) {
@@ -3408,12 +3424,24 @@ var init_spreadsheetRoutes = __esm({
3408
3424
  const controlIdStr = String(controlId).slice(0, 50);
3409
3425
  const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
3410
3426
  const filePath = join4(familyDir, fileName2);
3427
+ const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
3428
+ const mappingFilePath = join4(familyMappingsDir, mappingFileName);
3411
3429
  const filteredControl = {};
3430
+ const mappingData = {
3431
+ control_id: controlIdStr,
3432
+ justification: "",
3433
+ uuid: crypto.randomUUID()
3434
+ };
3435
+ const justificationContents = [];
3412
3436
  if (control.family !== void 0) {
3413
3437
  filteredControl.family = control.family;
3414
3438
  }
3415
3439
  Object.keys(control).forEach((fieldName) => {
3416
3440
  if (fieldName === "family") return;
3441
+ if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
3442
+ justificationContents.push(control[fieldName]);
3443
+ return;
3444
+ }
3417
3445
  const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
3418
3446
  const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
3419
3447
  if (isInFrontendSchema || isInFieldsMetadata) {
@@ -3421,6 +3449,13 @@ var init_spreadsheetRoutes = __esm({
3421
3449
  }
3422
3450
  });
3423
3451
  writeFileSync2(filePath, yaml4.dump(filteredControl));
3452
+ if (justificationContents.length > 0) {
3453
+ mappingData.justification = justificationContents.join("\n\n");
3454
+ }
3455
+ if (mappingData.justification && mappingData.justification.trim() !== "") {
3456
+ const mappingArray = [mappingData];
3457
+ writeFileSync2(mappingFilePath, yaml4.dump(mappingArray));
3458
+ }
3424
3459
  });
3425
3460
  });
3426
3461
  res.json({
@@ -4537,8 +4572,8 @@ var WebSocketManager = class {
4537
4572
  if (payload && payload.control_id) {
4538
4573
  const mapping = payload;
4539
4574
  if (!mapping.uuid) {
4540
- const crypto = await import("crypto");
4541
- mapping.uuid = crypto.randomUUID();
4575
+ const crypto2 = await import("crypto");
4576
+ mapping.uuid = crypto2.randomUUID();
4542
4577
  }
4543
4578
  await state.fileStore.saveMapping(mapping);
4544
4579
  state.mappingsCache.set(mapping.uuid, mapping);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lula2",
3
- "version": "0.1.1-nightly.0",
3
+ "version": "0.2.0",
4
4
  "description": "A tool for managing compliance as code in your GitHub repositories.",
5
5
  "bin": {
6
6
  "lula2": "./dist/lula2"
@@ -33,6 +33,25 @@
33
33
  "!dist/**/*.test.js*",
34
34
  "!dist/**/*.test.d.ts*"
35
35
  ],
36
+ "scripts": {
37
+ "dev": "vite dev --port 5173",
38
+ "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
39
+ "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
40
+ "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
41
+ "build:svelte": "vite build",
42
+ "build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
43
+ "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
44
+ "preview": "vite preview",
45
+ "prepare": "svelte-kit sync || echo ''",
46
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
47
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
48
+ "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
49
+ "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
50
+ "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
51
+ "test": "npm run test:unit -- --run --coverage",
52
+ "test:integration": "vitest --config integration/vitest.config.integration.ts",
53
+ "test:unit": "vitest"
54
+ },
36
55
  "dependencies": {
37
56
  "@octokit/rest": "^22.0.0",
38
57
  "@types/ws": "^8.18.1",
@@ -104,23 +123,5 @@
104
123
  "main",
105
124
  "next"
106
125
  ]
107
- },
108
- "scripts": {
109
- "dev": "vite dev --port 5173",
110
- "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
111
- "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
112
- "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
113
- "build:svelte": "vite build",
114
- "build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
115
- "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
116
- "preview": "vite preview",
117
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
118
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
119
- "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
120
- "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
121
- "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
122
- "test": "npm run test:unit -- --run --coverage",
123
- "test:integration": "vitest --config integration/vitest.config.integration.ts",
124
- "test:unit": "vitest"
125
126
  }
126
- }
127
+ }
@@ -17,7 +17,10 @@
17
17
  $: availableFields = fields.filter((f) => fields.includes(f));
18
18
 
19
19
  // Field configuration for tabs
20
- type TabAssignment = 'overview' | 'implementation' | 'custom' | null;
20
+ type TabAssignment = 'overview' | 'implementation' | 'mappings' | 'custom' | null;
21
+
22
+ // Store fields for justification
23
+ let justificationFields: string[] = [];
21
24
  let fieldConfigs = new Map<
22
25
  string,
23
26
  {
@@ -55,6 +58,7 @@
55
58
  controlCount = 0;
56
59
  fieldConfigs.clear();
57
60
  fieldConfigs = new Map(); // Force reactivity
61
+ justificationFields = [];
58
62
 
59
63
  // Reset selections
60
64
  controlIdField = '';
@@ -307,6 +311,17 @@
307
311
  e.preventDefault();
308
312
  if (draggedField && fieldConfigs.has(draggedField)) {
309
313
  const config = fieldConfigs.get(draggedField)!;
314
+
315
+ // If dragging from mappings tab to another tab, remove from justificationFields
316
+ if (config.tab === 'mappings' && tab !== 'mappings') {
317
+ justificationFields = justificationFields.filter((f) => f !== draggedField);
318
+ }
319
+
320
+ // If dragging to mappings tab, add to justificationFields
321
+ if (tab === 'mappings' && !justificationFields.includes(draggedField)) {
322
+ justificationFields = [...justificationFields, draggedField];
323
+ }
324
+
310
325
  config.tab = tab;
311
326
 
312
327
  // If dropping at a specific position, update display orders
@@ -420,6 +435,12 @@
420
435
  }));
421
436
  formData.append('fieldSchema', JSON.stringify(fieldSchema));
422
437
 
438
+ // Add justification fields
439
+ formData.append(
440
+ 'justificationFields',
441
+ JSON.stringify(justificationFields.map((field) => cleanFieldName(field)))
442
+ );
443
+
423
444
  const response = await fetch('/api/import-spreadsheet', {
424
445
  method: 'POST',
425
446
  body: formData
@@ -692,7 +713,7 @@
692
713
  </p>
693
714
 
694
715
  <!-- Column Layout -->
695
- <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
716
+ <div class="grid grid-cols-1 lg:grid-cols-5 gap-4">
696
717
  <!-- Excluded Fields Column -->
697
718
  <div
698
719
  class="border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
@@ -843,6 +864,88 @@
843
864
  </div>
844
865
  </div>
845
866
 
867
+ <!-- Mappings Tab Column -->
868
+ <div
869
+ class="border border-orange-300 dark:border-orange-700 rounded-lg bg-white dark:bg-gray-800"
870
+ >
871
+ <div
872
+ class="p-3 border-b border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-900/20 rounded-t-lg"
873
+ >
874
+ <h4 class="text-sm font-semibold text-orange-700 dark:text-orange-300">Mappings Tab</h4>
875
+ <p class="text-xs text-orange-600 dark:text-orange-400 mt-1">
876
+ Pre-populate justification for a control mapping
877
+ </p>
878
+ </div>
879
+ <div
880
+ class="p-3 min-h-[400px] max-h-[600px] overflow-y-auto transition-colors
881
+ {dragOverTab === 'mappings' ? 'bg-orange-50 dark:bg-orange-900/10' : ''}"
882
+ on:dragover={(e) => handleTabDragOver(e, 'mappings')}
883
+ on:dragleave={handleTabDragLeave}
884
+ on:drop={(e) => handleTabDrop(e, 'mappings')}
885
+ role="region"
886
+ aria-label="Justifications tab drop zone"
887
+ >
888
+ <!-- Justification Fields -->
889
+ <div class="space-y-2">
890
+ {#if justificationFields.length > 0}
891
+ <!-- Display justification fields -->
892
+ {#each justificationFields as field, index}
893
+ <div
894
+ draggable="true"
895
+ on:dragstart={(e) => handleFieldDragStart(e, field)}
896
+ on:dragend={handleFieldDragEnd}
897
+ on:dragover={(e) => handleFieldDragOver(e, field)}
898
+ on:dragleave={handleFieldDragLeave}
899
+ on:drop={(e) => handleFieldDrop(e, field, 'mappings')}
900
+ role="button"
901
+ aria-label="{field} field in Mappings tab"
902
+ tabindex="0"
903
+ class="flex items-center px-3 py-2 bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 rounded text-sm cursor-move hover:bg-orange-200 dark:hover:bg-orange-800/30 transition-colors
904
+ {dragOverField === field && draggedField !== field ? 'border-t-2 border-orange-500' : ''}"
905
+ >
906
+ <Draggable class="w-3 h-3 mr-2 flex-shrink-0" />
907
+ <span class="truncate">{field}</span>
908
+ </div>
909
+ {/each}
910
+ {:else}
911
+ <!-- Drop zone only shown when no fields are present -->
912
+ <div
913
+ role="region"
914
+ aria-label="Justification field drop zone"
915
+ class="p-4 transition-colors
916
+ {dragOverTab === 'mappings' ? 'bg-orange-50 dark:bg-orange-900/10' : ''}"
917
+ on:dragover={(e) => {
918
+ e.preventDefault();
919
+ handleTabDragOver(e, 'mappings');
920
+ }}
921
+ on:dragleave={handleTabDragLeave}
922
+ on:drop={(e) => {
923
+ e.preventDefault();
924
+ if (draggedField && fieldConfigs.has(draggedField)) {
925
+ // Add to justification fields if not already present
926
+ if (!justificationFields.includes(draggedField)) {
927
+ justificationFields = [...justificationFields, draggedField];
928
+ }
929
+
930
+ // Set tab assignment
931
+ const config = fieldConfigs.get(draggedField)!;
932
+ config.tab = 'mappings';
933
+ fieldConfigs.set(draggedField, config);
934
+ fieldConfigs = new Map(fieldConfigs); // Force reactivity
935
+
936
+ dragOverTab = null;
937
+ }
938
+ }}
939
+ >
940
+ <p class="text-xs text-gray-400 dark:text-gray-500 text-center py-4">
941
+ Drop fields here
942
+ </p>
943
+ </div>
944
+ {/if}
945
+ </div>
946
+ </div>
947
+ </div>
948
+
846
949
  <!-- Custom Tab Column -->
847
950
  <div
848
951
  class="border border-purple-300 dark:border-purple-700 rounded-lg bg-white dark:bg-gray-800"