markform 0.1.22 → 0.1.24
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 +53 -16
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +48 -6
- package/dist/ai-sdk.mjs.map +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/{cli-C8F9yDsv.mjs → cli-B1DhFYBS.mjs} +642 -96
- package/dist/cli-B1DhFYBS.mjs.map +1 -0
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-CTLr-NGd.mjs → coreTypes-CctFK6uE.mjs} +38 -2
- package/dist/coreTypes-CctFK6uE.mjs.map +1 -0
- package/dist/{coreTypes-BlsJkU1w.d.mts → coreTypes-GxzWNXap.d.mts} +137 -3
- package/dist/{fillRecord-DTl5lnK0.d.mts → fillRecord-DeqI2pQ5.d.mts} +25 -1
- package/dist/{fillRecordRenderer-CruJrLkj.mjs → fillRecordRenderer-VBQ2vwPV.mjs} +2 -5
- package/dist/fillRecordRenderer-VBQ2vwPV.mjs.map +1 -0
- package/dist/index.d.mts +52 -29
- package/dist/index.mjs +5 -5
- package/dist/{apply-C7mO7VkZ.mjs → prompts-BCnYaH4_.mjs} +969 -8
- package/dist/prompts-BCnYaH4_.mjs.map +1 -0
- package/dist/render.d.mts +2 -2
- package/dist/render.mjs +1 -1
- package/dist/{session-BCcltrLA.mjs → session-BLjN3BkJ.mjs} +2 -2
- package/dist/{session-BCcltrLA.mjs.map → session-BLjN3BkJ.mjs.map} +1 -1
- package/dist/{session-VeSkVrck.mjs → session-D7C7IlEv.mjs} +1 -1
- package/dist/{shared-CsdT2T7k.mjs → shared-CuSRYcIB.mjs} +3 -3
- package/dist/shared-CuSRYcIB.mjs.map +1 -0
- package/dist/{shared-fb0nkzQi.mjs → shared-DtorFV21.mjs} +1 -1
- package/dist/{src-CbRnGzMK.mjs → src-C5OWf1dL.mjs} +114 -826
- package/dist/src-C5OWf1dL.mjs.map +1 -0
- package/docs/markform-apis.md +6 -0
- package/docs/markform-reference.md +26 -1
- package/docs/markform-spec.md +11 -3
- package/docs/skill/SKILL.md +119 -0
- package/examples/rejection-test/rejection-test.session.yaml +52 -0
- package/examples/simple/simple-with-skips.session.yaml +78 -0
- package/examples/simple/simple.session.yaml +78 -0
- package/package.json +2 -2
- package/dist/apply-C7mO7VkZ.mjs.map +0 -1
- package/dist/cli-C8F9yDsv.mjs.map +0 -1
- package/dist/coreTypes-CTLr-NGd.mjs.map +0 -1
- package/dist/fillRecordRenderer-CruJrLkj.mjs.map +0 -1
- package/dist/shared-CsdT2T7k.mjs.map +0 -1
- package/dist/src-CbRnGzMK.mjs.map +0 -1
|
@@ -3,7 +3,7 @@ import { n as formatUrlAsMarkdownLink } from "./urlFormat-lls7CsEP.mjs";
|
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
|
|
5
5
|
//#region src/errors.ts
|
|
6
|
-
const VERSION = "0.1.
|
|
6
|
+
const VERSION = "0.1.24";
|
|
7
7
|
/**
|
|
8
8
|
* Base error class for all markform errors.
|
|
9
9
|
* Consumers can catch this to handle any markform error.
|
|
@@ -512,6 +512,12 @@ const DEFAULT_MAX_ISSUES_PER_TURN = 10;
|
|
|
512
512
|
*/
|
|
513
513
|
const DEFAULT_MAX_PARALLEL_AGENTS = 4;
|
|
514
514
|
/**
|
|
515
|
+
* Default maximum AI SDK retries for transient API errors (429, 503, etc.).
|
|
516
|
+
* The Vercel AI SDK handles retry with exponential backoff automatically.
|
|
517
|
+
* Set to 0 to disable retries (useful for fast tests).
|
|
518
|
+
*/
|
|
519
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
520
|
+
/**
|
|
515
521
|
* Default maximum AI SDK steps (tool call rounds) per harness turn.
|
|
516
522
|
* Matches AI SDK's ToolLoopAgent default of 20.
|
|
517
523
|
* @see https://ai-sdk.dev/docs/agents/loop-control
|
|
@@ -3772,7 +3778,7 @@ function getAllFields(form) {
|
|
|
3772
3778
|
/**
|
|
3773
3779
|
* Find a field by its ID.
|
|
3774
3780
|
*/
|
|
3775
|
-
function findFieldById(form, fieldId) {
|
|
3781
|
+
function findFieldById$1(form, fieldId) {
|
|
3776
3782
|
for (const group of form.schema.groups) for (const field of group.children) if (field.id === fieldId) return field;
|
|
3777
3783
|
}
|
|
3778
3784
|
/**
|
|
@@ -3793,7 +3799,7 @@ function getFieldsForRoles(form, targetRoles) {
|
|
|
3793
3799
|
* - 'explicit': No options may be 'unfilled'
|
|
3794
3800
|
*/
|
|
3795
3801
|
function isCheckboxComplete(form, fieldId) {
|
|
3796
|
-
const field = findFieldById(form, fieldId);
|
|
3802
|
+
const field = findFieldById$1(form, fieldId);
|
|
3797
3803
|
if (field?.kind !== "checkboxes") return true;
|
|
3798
3804
|
const checkboxField = field;
|
|
3799
3805
|
const response = form.responsesByFieldId[fieldId];
|
|
@@ -3820,7 +3826,7 @@ function findBlockingCheckpoint(form) {
|
|
|
3820
3826
|
for (let i = 0; i < form.orderIndex.length; i++) {
|
|
3821
3827
|
const fieldId = form.orderIndex[i];
|
|
3822
3828
|
if (!fieldId) continue;
|
|
3823
|
-
const field = findFieldById(form, fieldId);
|
|
3829
|
+
const field = findFieldById$1(form, fieldId);
|
|
3824
3830
|
if (field?.kind !== "checkboxes") continue;
|
|
3825
3831
|
if (field.approvalMode === "blocking" && !isCheckboxComplete(form, fieldId)) return {
|
|
3826
3832
|
index: i,
|
|
@@ -3858,7 +3864,7 @@ function filterIssuesByRole(issues, form, targetRoles) {
|
|
|
3858
3864
|
}).filter((issue) => {
|
|
3859
3865
|
if (!targetFieldIds) return true;
|
|
3860
3866
|
const fieldId = issue.ref.includes(".") ? issue.ref.split(".")[0] ?? issue.ref : issue.ref;
|
|
3861
|
-
return !findFieldById(form, fieldId) || targetFieldIds.has(fieldId);
|
|
3867
|
+
return !findFieldById$1(form, fieldId) || targetFieldIds.has(fieldId);
|
|
3862
3868
|
});
|
|
3863
3869
|
}
|
|
3864
3870
|
|
|
@@ -3876,7 +3882,13 @@ const PATCH_OP_TO_FIELD_KIND = {
|
|
|
3876
3882
|
set_url_list: "url_list",
|
|
3877
3883
|
set_date: "date",
|
|
3878
3884
|
set_year: "year",
|
|
3879
|
-
set_table: "table"
|
|
3885
|
+
set_table: "table",
|
|
3886
|
+
append_table: "table",
|
|
3887
|
+
delete_table: "table",
|
|
3888
|
+
append_string_list: "string_list",
|
|
3889
|
+
delete_string_list: "string_list",
|
|
3890
|
+
append_url_list: "url_list",
|
|
3891
|
+
delete_url_list: "url_list"
|
|
3880
3892
|
};
|
|
3881
3893
|
/**
|
|
3882
3894
|
* Find a field by ID in the form schema.
|
|
@@ -4101,6 +4113,43 @@ function validatePatch(form, patch, index) {
|
|
|
4101
4113
|
message: `Invalid column "${colId}" for table field "${field.id}"`
|
|
4102
4114
|
};
|
|
4103
4115
|
}
|
|
4116
|
+
} else if (patch.op === "append_table" && field.kind === "table") {
|
|
4117
|
+
const columnIds = field.columns.map((c) => c.id);
|
|
4118
|
+
if (!Array.isArray(patch.value)) return {
|
|
4119
|
+
patchIndex: index,
|
|
4120
|
+
message: `Invalid append_table patch for field "${field.id}": value must be an array of row objects. Columns: [${columnIds.join(", ")}]`,
|
|
4121
|
+
fieldId: field.id,
|
|
4122
|
+
fieldKind: field.kind,
|
|
4123
|
+
columnIds
|
|
4124
|
+
};
|
|
4125
|
+
const validColumns = new Set(columnIds);
|
|
4126
|
+
for (const row of patch.value) if (row != null) {
|
|
4127
|
+
for (const colId of Object.keys(row)) if (!validColumns.has(colId)) return {
|
|
4128
|
+
patchIndex: index,
|
|
4129
|
+
message: `Invalid column "${colId}" for table field "${field.id}"`
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
} else if (patch.op === "delete_table" && field.kind === "table") {
|
|
4133
|
+
const currentValue = form.responsesByFieldId[field.id]?.value;
|
|
4134
|
+
const rowCount = currentValue?.kind === "table" ? currentValue.rows.length : 0;
|
|
4135
|
+
if (patch.value >= rowCount) return {
|
|
4136
|
+
patchIndex: index,
|
|
4137
|
+
message: `Index ${patch.value} out of bounds for table field "${field.id}" (${rowCount} rows)`
|
|
4138
|
+
};
|
|
4139
|
+
} else if ((patch.op === "append_string_list" || patch.op === "append_url_list") && (field.kind === "string_list" || field.kind === "url_list")) {
|
|
4140
|
+
if (!Array.isArray(patch.value)) return {
|
|
4141
|
+
patchIndex: index,
|
|
4142
|
+
message: `Invalid ${patch.op} patch for field "${field.id}": value must be an array`,
|
|
4143
|
+
fieldId: field.id,
|
|
4144
|
+
fieldKind: field.kind
|
|
4145
|
+
};
|
|
4146
|
+
} else if ((patch.op === "delete_string_list" || patch.op === "delete_url_list") && (field.kind === "string_list" || field.kind === "url_list")) {
|
|
4147
|
+
const currentValue = form.responsesByFieldId[field.id]?.value;
|
|
4148
|
+
const itemCount = currentValue?.kind === "string_list" || currentValue?.kind === "url_list" ? currentValue.items.length : 0;
|
|
4149
|
+
if (patch.value >= itemCount) return {
|
|
4150
|
+
patchIndex: index,
|
|
4151
|
+
message: `Index ${patch.value} out of bounds for ${field.kind} field "${field.id}" (${itemCount} items)`
|
|
4152
|
+
};
|
|
4104
4153
|
} else if (patch.op === "skip_field" && field.required) return {
|
|
4105
4154
|
patchIndex: index,
|
|
4106
4155
|
message: `Cannot skip required field "${field.id}"`
|
|
@@ -4247,6 +4296,86 @@ function applyPatch(form, responses, patch) {
|
|
|
4247
4296
|
};
|
|
4248
4297
|
break;
|
|
4249
4298
|
}
|
|
4299
|
+
case "append_table": {
|
|
4300
|
+
const existing = responses[patch.fieldId]?.value;
|
|
4301
|
+
const currentRows = existing?.kind === "table" ? [...existing.rows] : [];
|
|
4302
|
+
const newRows = (patch.value ?? []).map((patchRow) => {
|
|
4303
|
+
const row = {};
|
|
4304
|
+
if (patchRow != null) for (const [colId, cellValue] of Object.entries(patchRow)) row[colId] = patchValueToCell(cellValue);
|
|
4305
|
+
return row;
|
|
4306
|
+
});
|
|
4307
|
+
responses[patch.fieldId] = {
|
|
4308
|
+
state: "answered",
|
|
4309
|
+
value: {
|
|
4310
|
+
kind: "table",
|
|
4311
|
+
rows: [...currentRows, ...newRows]
|
|
4312
|
+
}
|
|
4313
|
+
};
|
|
4314
|
+
break;
|
|
4315
|
+
}
|
|
4316
|
+
case "delete_table": {
|
|
4317
|
+
const existingTbl = responses[patch.fieldId]?.value;
|
|
4318
|
+
const rows = existingTbl?.kind === "table" ? [...existingTbl.rows] : [];
|
|
4319
|
+
rows.splice(patch.value, 1);
|
|
4320
|
+
responses[patch.fieldId] = {
|
|
4321
|
+
state: rows.length > 0 ? "answered" : "unanswered",
|
|
4322
|
+
...rows.length > 0 && { value: {
|
|
4323
|
+
kind: "table",
|
|
4324
|
+
rows
|
|
4325
|
+
} }
|
|
4326
|
+
};
|
|
4327
|
+
break;
|
|
4328
|
+
}
|
|
4329
|
+
case "append_string_list": {
|
|
4330
|
+
const existingList = responses[patch.fieldId]?.value;
|
|
4331
|
+
const currentItems = existingList?.kind === "string_list" ? [...existingList.items] : [];
|
|
4332
|
+
responses[patch.fieldId] = {
|
|
4333
|
+
state: "answered",
|
|
4334
|
+
value: {
|
|
4335
|
+
kind: "string_list",
|
|
4336
|
+
items: [...currentItems, ...patch.value]
|
|
4337
|
+
}
|
|
4338
|
+
};
|
|
4339
|
+
break;
|
|
4340
|
+
}
|
|
4341
|
+
case "delete_string_list": {
|
|
4342
|
+
const existingSl = responses[patch.fieldId]?.value;
|
|
4343
|
+
const items = existingSl?.kind === "string_list" ? [...existingSl.items] : [];
|
|
4344
|
+
items.splice(patch.value, 1);
|
|
4345
|
+
responses[patch.fieldId] = {
|
|
4346
|
+
state: items.length > 0 ? "answered" : "unanswered",
|
|
4347
|
+
...items.length > 0 && { value: {
|
|
4348
|
+
kind: "string_list",
|
|
4349
|
+
items
|
|
4350
|
+
} }
|
|
4351
|
+
};
|
|
4352
|
+
break;
|
|
4353
|
+
}
|
|
4354
|
+
case "append_url_list": {
|
|
4355
|
+
const existingUl = responses[patch.fieldId]?.value;
|
|
4356
|
+
const currentUrls = existingUl?.kind === "url_list" ? [...existingUl.items] : [];
|
|
4357
|
+
responses[patch.fieldId] = {
|
|
4358
|
+
state: "answered",
|
|
4359
|
+
value: {
|
|
4360
|
+
kind: "url_list",
|
|
4361
|
+
items: [...currentUrls, ...patch.value]
|
|
4362
|
+
}
|
|
4363
|
+
};
|
|
4364
|
+
break;
|
|
4365
|
+
}
|
|
4366
|
+
case "delete_url_list": {
|
|
4367
|
+
const existingUlDel = responses[patch.fieldId]?.value;
|
|
4368
|
+
const urls = existingUlDel?.kind === "url_list" ? [...existingUlDel.items] : [];
|
|
4369
|
+
urls.splice(patch.value, 1);
|
|
4370
|
+
responses[patch.fieldId] = {
|
|
4371
|
+
state: urls.length > 0 ? "answered" : "unanswered",
|
|
4372
|
+
...urls.length > 0 && { value: {
|
|
4373
|
+
kind: "url_list",
|
|
4374
|
+
items: urls
|
|
4375
|
+
} }
|
|
4376
|
+
};
|
|
4377
|
+
break;
|
|
4378
|
+
}
|
|
4250
4379
|
case "clear_field":
|
|
4251
4380
|
responses[patch.fieldId] = { state: "unanswered" };
|
|
4252
4381
|
break;
|
|
@@ -4356,5 +4485,837 @@ function applyPatches(form, patches) {
|
|
|
4356
4485
|
}
|
|
4357
4486
|
|
|
4358
4487
|
//#endregion
|
|
4359
|
-
|
|
4360
|
-
|
|
4488
|
+
//#region src/engine/valueCoercion.ts
|
|
4489
|
+
/**
|
|
4490
|
+
* Find a field by ID.
|
|
4491
|
+
*
|
|
4492
|
+
* Uses idIndex for O(1) validation that the ID exists and is a field,
|
|
4493
|
+
* then retrieves the Field object from the schema.
|
|
4494
|
+
*/
|
|
4495
|
+
function findFieldById(form, fieldId) {
|
|
4496
|
+
if (form.idIndex.get(fieldId)?.nodeType !== "field") return;
|
|
4497
|
+
for (const group of form.schema.groups) for (const field of group.children) if (field.id === fieldId) return field;
|
|
4498
|
+
}
|
|
4499
|
+
function isPlainObject(value) {
|
|
4500
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4501
|
+
}
|
|
4502
|
+
function isStringArray(value) {
|
|
4503
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
4504
|
+
}
|
|
4505
|
+
function coerceToString(fieldId, rawValue) {
|
|
4506
|
+
if (rawValue === null) return {
|
|
4507
|
+
ok: true,
|
|
4508
|
+
patch: {
|
|
4509
|
+
op: "set_string",
|
|
4510
|
+
fieldId,
|
|
4511
|
+
value: null
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
if (typeof rawValue === "string") return {
|
|
4515
|
+
ok: true,
|
|
4516
|
+
patch: {
|
|
4517
|
+
op: "set_string",
|
|
4518
|
+
fieldId,
|
|
4519
|
+
value: rawValue
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
if (typeof rawValue === "number") return {
|
|
4523
|
+
ok: true,
|
|
4524
|
+
patch: {
|
|
4525
|
+
op: "set_string",
|
|
4526
|
+
fieldId,
|
|
4527
|
+
value: String(rawValue)
|
|
4528
|
+
},
|
|
4529
|
+
warning: `Coerced number ${rawValue} to string for field '${fieldId}'`
|
|
4530
|
+
};
|
|
4531
|
+
if (typeof rawValue === "boolean") return {
|
|
4532
|
+
ok: true,
|
|
4533
|
+
patch: {
|
|
4534
|
+
op: "set_string",
|
|
4535
|
+
fieldId,
|
|
4536
|
+
value: String(rawValue)
|
|
4537
|
+
},
|
|
4538
|
+
warning: `Coerced boolean ${rawValue} to string for field '${fieldId}'`
|
|
4539
|
+
};
|
|
4540
|
+
return {
|
|
4541
|
+
ok: false,
|
|
4542
|
+
error: `Cannot coerce ${typeof rawValue} to string for field '${fieldId}'`
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
function coerceToNumber(fieldId, rawValue) {
|
|
4546
|
+
if (rawValue === null) return {
|
|
4547
|
+
ok: true,
|
|
4548
|
+
patch: {
|
|
4549
|
+
op: "set_number",
|
|
4550
|
+
fieldId,
|
|
4551
|
+
value: null
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
if (typeof rawValue === "number") return {
|
|
4555
|
+
ok: true,
|
|
4556
|
+
patch: {
|
|
4557
|
+
op: "set_number",
|
|
4558
|
+
fieldId,
|
|
4559
|
+
value: rawValue
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
if (typeof rawValue === "string") {
|
|
4563
|
+
const parsed = Number(rawValue);
|
|
4564
|
+
if (!Number.isNaN(parsed)) return {
|
|
4565
|
+
ok: true,
|
|
4566
|
+
patch: {
|
|
4567
|
+
op: "set_number",
|
|
4568
|
+
fieldId,
|
|
4569
|
+
value: parsed
|
|
4570
|
+
},
|
|
4571
|
+
warning: `Coerced string '${rawValue}' to number for field '${fieldId}'`
|
|
4572
|
+
};
|
|
4573
|
+
return {
|
|
4574
|
+
ok: false,
|
|
4575
|
+
error: `Cannot coerce non-numeric string '${rawValue}' to number for field '${fieldId}'`
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
return {
|
|
4579
|
+
ok: false,
|
|
4580
|
+
error: `Cannot coerce ${typeof rawValue} to number for field '${fieldId}'`
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
function coerceToStringList(fieldId, rawValue) {
|
|
4584
|
+
if (rawValue === null) return {
|
|
4585
|
+
ok: true,
|
|
4586
|
+
patch: {
|
|
4587
|
+
op: "set_string_list",
|
|
4588
|
+
fieldId,
|
|
4589
|
+
value: []
|
|
4590
|
+
}
|
|
4591
|
+
};
|
|
4592
|
+
if (isStringArray(rawValue)) return {
|
|
4593
|
+
ok: true,
|
|
4594
|
+
patch: {
|
|
4595
|
+
op: "set_string_list",
|
|
4596
|
+
fieldId,
|
|
4597
|
+
value: rawValue
|
|
4598
|
+
}
|
|
4599
|
+
};
|
|
4600
|
+
if (typeof rawValue === "string") return {
|
|
4601
|
+
ok: true,
|
|
4602
|
+
patch: {
|
|
4603
|
+
op: "set_string_list",
|
|
4604
|
+
fieldId,
|
|
4605
|
+
value: [rawValue]
|
|
4606
|
+
},
|
|
4607
|
+
warning: `Coerced single string to array for field '${fieldId}'`
|
|
4608
|
+
};
|
|
4609
|
+
if (Array.isArray(rawValue)) {
|
|
4610
|
+
const items = [];
|
|
4611
|
+
for (const item of rawValue) if (typeof item === "string") items.push(item);
|
|
4612
|
+
else if (typeof item === "number" || typeof item === "boolean") items.push(String(item));
|
|
4613
|
+
else return {
|
|
4614
|
+
ok: false,
|
|
4615
|
+
error: `Cannot coerce array with non-string items to string_list for field '${fieldId}'`
|
|
4616
|
+
};
|
|
4617
|
+
return {
|
|
4618
|
+
ok: true,
|
|
4619
|
+
patch: {
|
|
4620
|
+
op: "set_string_list",
|
|
4621
|
+
fieldId,
|
|
4622
|
+
value: items
|
|
4623
|
+
},
|
|
4624
|
+
warning: `Coerced array items to strings for field '${fieldId}'`
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
return {
|
|
4628
|
+
ok: false,
|
|
4629
|
+
error: `Cannot coerce ${typeof rawValue} to string_list for field '${fieldId}'`
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
function coerceToSingleSelect(field, rawValue) {
|
|
4633
|
+
if (field.kind !== "single_select") return {
|
|
4634
|
+
ok: false,
|
|
4635
|
+
error: `Field '${field.id}' is not a single_select field`
|
|
4636
|
+
};
|
|
4637
|
+
if (rawValue === null) return {
|
|
4638
|
+
ok: true,
|
|
4639
|
+
patch: {
|
|
4640
|
+
op: "set_single_select",
|
|
4641
|
+
fieldId: field.id,
|
|
4642
|
+
value: null
|
|
4643
|
+
}
|
|
4644
|
+
};
|
|
4645
|
+
if (typeof rawValue !== "string") return {
|
|
4646
|
+
ok: false,
|
|
4647
|
+
error: `single_select field '${field.id}' requires a string option ID, got ${typeof rawValue}`
|
|
4648
|
+
};
|
|
4649
|
+
const validOptions = new Set(field.options.map((o) => o.id));
|
|
4650
|
+
if (!validOptions.has(rawValue)) return {
|
|
4651
|
+
ok: false,
|
|
4652
|
+
error: `Invalid option '${rawValue}' for single_select field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
4653
|
+
};
|
|
4654
|
+
return {
|
|
4655
|
+
ok: true,
|
|
4656
|
+
patch: {
|
|
4657
|
+
op: "set_single_select",
|
|
4658
|
+
fieldId: field.id,
|
|
4659
|
+
value: rawValue
|
|
4660
|
+
}
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
function coerceToMultiSelect(field, rawValue) {
|
|
4664
|
+
if (field.kind !== "multi_select") return {
|
|
4665
|
+
ok: false,
|
|
4666
|
+
error: `Field '${field.id}' is not a multi_select field`
|
|
4667
|
+
};
|
|
4668
|
+
if (rawValue === null) return {
|
|
4669
|
+
ok: true,
|
|
4670
|
+
patch: {
|
|
4671
|
+
op: "set_multi_select",
|
|
4672
|
+
fieldId: field.id,
|
|
4673
|
+
value: []
|
|
4674
|
+
}
|
|
4675
|
+
};
|
|
4676
|
+
const validOptions = new Set(field.options.map((o) => o.id));
|
|
4677
|
+
let selected;
|
|
4678
|
+
let warning;
|
|
4679
|
+
if (typeof rawValue === "string") {
|
|
4680
|
+
selected = [rawValue];
|
|
4681
|
+
warning = `Coerced single string to array for multi_select field '${field.id}'`;
|
|
4682
|
+
} else if (isStringArray(rawValue)) selected = rawValue;
|
|
4683
|
+
else return {
|
|
4684
|
+
ok: false,
|
|
4685
|
+
error: `multi_select field '${field.id}' requires a string or string array, got ${typeof rawValue}`
|
|
4686
|
+
};
|
|
4687
|
+
for (const optId of selected) if (!validOptions.has(optId)) return {
|
|
4688
|
+
ok: false,
|
|
4689
|
+
error: `Invalid option '${optId}' for multi_select field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
4690
|
+
};
|
|
4691
|
+
const patch = {
|
|
4692
|
+
op: "set_multi_select",
|
|
4693
|
+
fieldId: field.id,
|
|
4694
|
+
value: selected
|
|
4695
|
+
};
|
|
4696
|
+
return warning ? {
|
|
4697
|
+
ok: true,
|
|
4698
|
+
patch,
|
|
4699
|
+
warning
|
|
4700
|
+
} : {
|
|
4701
|
+
ok: true,
|
|
4702
|
+
patch
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
function coerceToCheckboxes(field, rawValue) {
|
|
4706
|
+
if (field.kind !== "checkboxes") return {
|
|
4707
|
+
ok: false,
|
|
4708
|
+
error: `Field '${field.id}' is not a checkboxes field`
|
|
4709
|
+
};
|
|
4710
|
+
if (rawValue === null) return {
|
|
4711
|
+
ok: true,
|
|
4712
|
+
patch: {
|
|
4713
|
+
op: "set_checkboxes",
|
|
4714
|
+
fieldId: field.id,
|
|
4715
|
+
value: {}
|
|
4716
|
+
}
|
|
4717
|
+
};
|
|
4718
|
+
const validOptions = new Set(field.options.map((o) => o.id));
|
|
4719
|
+
const checkboxMode = field.checkboxMode;
|
|
4720
|
+
if (Array.isArray(rawValue)) {
|
|
4721
|
+
const defaultState = checkboxMode === "explicit" ? "yes" : "done";
|
|
4722
|
+
const values = {};
|
|
4723
|
+
for (const item of rawValue) {
|
|
4724
|
+
if (typeof item !== "string") return {
|
|
4725
|
+
ok: false,
|
|
4726
|
+
error: `Array items for checkboxes field '${field.id}' must be strings (option IDs), got ${typeof item}`
|
|
4727
|
+
};
|
|
4728
|
+
if (!validOptions.has(item)) return {
|
|
4729
|
+
ok: false,
|
|
4730
|
+
error: `Invalid option '${item}' for checkboxes field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
4731
|
+
};
|
|
4732
|
+
values[item] = defaultState;
|
|
4733
|
+
}
|
|
4734
|
+
const patch = {
|
|
4735
|
+
op: "set_checkboxes",
|
|
4736
|
+
fieldId: field.id,
|
|
4737
|
+
value: values
|
|
4738
|
+
};
|
|
4739
|
+
if (rawValue.length === 0) return {
|
|
4740
|
+
ok: true,
|
|
4741
|
+
patch
|
|
4742
|
+
};
|
|
4743
|
+
return {
|
|
4744
|
+
ok: true,
|
|
4745
|
+
patch,
|
|
4746
|
+
warning: `Coerced array to checkboxes object with '${defaultState}' state for field '${field.id}'`
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
if (!isPlainObject(rawValue)) return {
|
|
4750
|
+
ok: false,
|
|
4751
|
+
error: `checkboxes field '${field.id}' requires a Record<string, CheckboxValue> or array of option IDs, got ${typeof rawValue}`
|
|
4752
|
+
};
|
|
4753
|
+
const values = {};
|
|
4754
|
+
let hadBooleanCoercion = false;
|
|
4755
|
+
const validValues = new Set(checkboxMode === "explicit" ? [
|
|
4756
|
+
"unfilled",
|
|
4757
|
+
"yes",
|
|
4758
|
+
"no"
|
|
4759
|
+
] : checkboxMode === "simple" ? ["todo", "done"] : [
|
|
4760
|
+
"todo",
|
|
4761
|
+
"done",
|
|
4762
|
+
"incomplete",
|
|
4763
|
+
"active",
|
|
4764
|
+
"na"
|
|
4765
|
+
]);
|
|
4766
|
+
for (const [optId, value] of Object.entries(rawValue)) {
|
|
4767
|
+
if (!validOptions.has(optId)) return {
|
|
4768
|
+
ok: false,
|
|
4769
|
+
error: `Invalid option '${optId}' for checkboxes field '${field.id}'. Valid options: ${Array.from(validOptions).join(", ")}`
|
|
4770
|
+
};
|
|
4771
|
+
if (typeof value === "boolean") {
|
|
4772
|
+
hadBooleanCoercion = true;
|
|
4773
|
+
if (checkboxMode === "explicit") values[optId] = value ? "yes" : "no";
|
|
4774
|
+
else values[optId] = value ? "done" : "todo";
|
|
4775
|
+
continue;
|
|
4776
|
+
}
|
|
4777
|
+
if (typeof value !== "string" || !validValues.has(value)) return {
|
|
4778
|
+
ok: false,
|
|
4779
|
+
error: `Invalid checkbox value '${String(value)}' for option '${optId}' in field '${field.id}'. Valid values for ${checkboxMode} mode: ${Array.from(validValues).join(", ")} (or use true/false)`
|
|
4780
|
+
};
|
|
4781
|
+
values[optId] = value;
|
|
4782
|
+
}
|
|
4783
|
+
const patch = {
|
|
4784
|
+
op: "set_checkboxes",
|
|
4785
|
+
fieldId: field.id,
|
|
4786
|
+
value: values
|
|
4787
|
+
};
|
|
4788
|
+
if (hadBooleanCoercion) return {
|
|
4789
|
+
ok: true,
|
|
4790
|
+
patch,
|
|
4791
|
+
warning: `Coerced boolean values to checkbox strings for field '${field.id}'`
|
|
4792
|
+
};
|
|
4793
|
+
return {
|
|
4794
|
+
ok: true,
|
|
4795
|
+
patch
|
|
4796
|
+
};
|
|
4797
|
+
}
|
|
4798
|
+
function coerceToUrl(fieldId, rawValue) {
|
|
4799
|
+
if (rawValue === null) return {
|
|
4800
|
+
ok: true,
|
|
4801
|
+
patch: {
|
|
4802
|
+
op: "set_url",
|
|
4803
|
+
fieldId,
|
|
4804
|
+
value: null
|
|
4805
|
+
}
|
|
4806
|
+
};
|
|
4807
|
+
if (typeof rawValue === "string") return {
|
|
4808
|
+
ok: true,
|
|
4809
|
+
patch: {
|
|
4810
|
+
op: "set_url",
|
|
4811
|
+
fieldId,
|
|
4812
|
+
value: rawValue
|
|
4813
|
+
}
|
|
4814
|
+
};
|
|
4815
|
+
return {
|
|
4816
|
+
ok: false,
|
|
4817
|
+
error: `Cannot coerce ${typeof rawValue} to url for field '${fieldId}'`
|
|
4818
|
+
};
|
|
4819
|
+
}
|
|
4820
|
+
function coerceToUrlList(fieldId, rawValue) {
|
|
4821
|
+
if (rawValue === null) return {
|
|
4822
|
+
ok: true,
|
|
4823
|
+
patch: {
|
|
4824
|
+
op: "set_url_list",
|
|
4825
|
+
fieldId,
|
|
4826
|
+
value: []
|
|
4827
|
+
}
|
|
4828
|
+
};
|
|
4829
|
+
if (isStringArray(rawValue)) return {
|
|
4830
|
+
ok: true,
|
|
4831
|
+
patch: {
|
|
4832
|
+
op: "set_url_list",
|
|
4833
|
+
fieldId,
|
|
4834
|
+
value: rawValue
|
|
4835
|
+
}
|
|
4836
|
+
};
|
|
4837
|
+
if (typeof rawValue === "string") return {
|
|
4838
|
+
ok: true,
|
|
4839
|
+
patch: {
|
|
4840
|
+
op: "set_url_list",
|
|
4841
|
+
fieldId,
|
|
4842
|
+
value: [rawValue]
|
|
4843
|
+
},
|
|
4844
|
+
warning: `Coerced single string to array for field '${fieldId}'`
|
|
4845
|
+
};
|
|
4846
|
+
if (Array.isArray(rawValue)) {
|
|
4847
|
+
const items = [];
|
|
4848
|
+
for (const item of rawValue) if (typeof item === "string") items.push(item);
|
|
4849
|
+
else return {
|
|
4850
|
+
ok: false,
|
|
4851
|
+
error: `Cannot coerce array with non-string items to url_list for field '${fieldId}'`
|
|
4852
|
+
};
|
|
4853
|
+
return {
|
|
4854
|
+
ok: true,
|
|
4855
|
+
patch: {
|
|
4856
|
+
op: "set_url_list",
|
|
4857
|
+
fieldId,
|
|
4858
|
+
value: items
|
|
4859
|
+
}
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
return {
|
|
4863
|
+
ok: false,
|
|
4864
|
+
error: `Cannot coerce ${typeof rawValue} to url_list for field '${fieldId}'`
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
function coerceToDate(fieldId, rawValue) {
|
|
4868
|
+
if (rawValue === null) return {
|
|
4869
|
+
ok: true,
|
|
4870
|
+
patch: {
|
|
4871
|
+
op: "set_date",
|
|
4872
|
+
fieldId,
|
|
4873
|
+
value: null
|
|
4874
|
+
}
|
|
4875
|
+
};
|
|
4876
|
+
if (typeof rawValue === "string") return {
|
|
4877
|
+
ok: true,
|
|
4878
|
+
patch: {
|
|
4879
|
+
op: "set_date",
|
|
4880
|
+
fieldId,
|
|
4881
|
+
value: rawValue
|
|
4882
|
+
}
|
|
4883
|
+
};
|
|
4884
|
+
return {
|
|
4885
|
+
ok: false,
|
|
4886
|
+
error: `Cannot coerce ${typeof rawValue} to date for field '${fieldId}'`
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
function coerceToYear(fieldId, rawValue) {
|
|
4890
|
+
if (rawValue === null) return {
|
|
4891
|
+
ok: true,
|
|
4892
|
+
patch: {
|
|
4893
|
+
op: "set_year",
|
|
4894
|
+
fieldId,
|
|
4895
|
+
value: null
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4898
|
+
if (typeof rawValue === "number") {
|
|
4899
|
+
if (!Number.isInteger(rawValue)) return {
|
|
4900
|
+
ok: false,
|
|
4901
|
+
error: `Year must be an integer for field '${fieldId}', got ${rawValue}`
|
|
4902
|
+
};
|
|
4903
|
+
return {
|
|
4904
|
+
ok: true,
|
|
4905
|
+
patch: {
|
|
4906
|
+
op: "set_year",
|
|
4907
|
+
fieldId,
|
|
4908
|
+
value: rawValue
|
|
4909
|
+
}
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
if (typeof rawValue === "string") {
|
|
4913
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
4914
|
+
if (Number.isNaN(parsed)) return {
|
|
4915
|
+
ok: false,
|
|
4916
|
+
error: `Cannot coerce non-numeric string '${rawValue}' to year for field '${fieldId}'`
|
|
4917
|
+
};
|
|
4918
|
+
return {
|
|
4919
|
+
ok: true,
|
|
4920
|
+
patch: {
|
|
4921
|
+
op: "set_year",
|
|
4922
|
+
fieldId,
|
|
4923
|
+
value: parsed
|
|
4924
|
+
},
|
|
4925
|
+
warning: `Coerced string '${rawValue}' to year for field '${fieldId}'`
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4928
|
+
return {
|
|
4929
|
+
ok: false,
|
|
4930
|
+
error: `Cannot coerce ${typeof rawValue} to year for field '${fieldId}'`
|
|
4931
|
+
};
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Coerce raw value to SetTablePatch.
|
|
4935
|
+
* Accepts:
|
|
4936
|
+
* - Array of row objects: [{ col1: value1, col2: value2 }, ...]
|
|
4937
|
+
* - Empty array: [] (valid for optional tables or minRows=0)
|
|
4938
|
+
*/
|
|
4939
|
+
function coerceToTable(fieldId, rawValue) {
|
|
4940
|
+
if (rawValue === null) return {
|
|
4941
|
+
ok: true,
|
|
4942
|
+
patch: {
|
|
4943
|
+
op: "set_table",
|
|
4944
|
+
fieldId,
|
|
4945
|
+
value: []
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
if (!Array.isArray(rawValue)) return {
|
|
4949
|
+
ok: false,
|
|
4950
|
+
error: `Table value for field '${fieldId}' must be an array of rows, got ${typeof rawValue}`
|
|
4951
|
+
};
|
|
4952
|
+
if (rawValue.length === 0) return {
|
|
4953
|
+
ok: true,
|
|
4954
|
+
patch: {
|
|
4955
|
+
op: "set_table",
|
|
4956
|
+
fieldId,
|
|
4957
|
+
value: []
|
|
4958
|
+
}
|
|
4959
|
+
};
|
|
4960
|
+
const rows = [];
|
|
4961
|
+
for (let i = 0; i < rawValue.length; i++) {
|
|
4962
|
+
const row = rawValue[i];
|
|
4963
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) return {
|
|
4964
|
+
ok: false,
|
|
4965
|
+
error: `Row ${i} for table field '${fieldId}' must be an object, got ${Array.isArray(row) ? "array" : typeof row}`
|
|
4966
|
+
};
|
|
4967
|
+
rows.push(row);
|
|
4968
|
+
}
|
|
4969
|
+
return {
|
|
4970
|
+
ok: true,
|
|
4971
|
+
patch: {
|
|
4972
|
+
op: "set_table",
|
|
4973
|
+
fieldId,
|
|
4974
|
+
value: rows
|
|
4975
|
+
}
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* Coerce a raw value to a Patch for a specific field.
|
|
4980
|
+
*/
|
|
4981
|
+
function coerceToFieldPatch(form, fieldId, rawValue) {
|
|
4982
|
+
const field = findFieldById(form, fieldId);
|
|
4983
|
+
if (!field) return {
|
|
4984
|
+
ok: false,
|
|
4985
|
+
error: `Field '${fieldId}' not found`
|
|
4986
|
+
};
|
|
4987
|
+
switch (field.kind) {
|
|
4988
|
+
case "string": return coerceToString(fieldId, rawValue);
|
|
4989
|
+
case "number": return coerceToNumber(fieldId, rawValue);
|
|
4990
|
+
case "string_list": return coerceToStringList(fieldId, rawValue);
|
|
4991
|
+
case "single_select": return coerceToSingleSelect(field, rawValue);
|
|
4992
|
+
case "multi_select": return coerceToMultiSelect(field, rawValue);
|
|
4993
|
+
case "checkboxes": return coerceToCheckboxes(field, rawValue);
|
|
4994
|
+
case "url": return coerceToUrl(fieldId, rawValue);
|
|
4995
|
+
case "url_list": return coerceToUrlList(fieldId, rawValue);
|
|
4996
|
+
case "date": return coerceToDate(fieldId, rawValue);
|
|
4997
|
+
case "year": return coerceToYear(fieldId, rawValue);
|
|
4998
|
+
case "table": return coerceToTable(fieldId, rawValue);
|
|
4999
|
+
default: {
|
|
5000
|
+
const _exhaustive = field;
|
|
5001
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
/**
|
|
5006
|
+
* Coerce an entire InputContext to patches.
|
|
5007
|
+
*
|
|
5008
|
+
* Returns patches for valid entries, collects warnings for coercions,
|
|
5009
|
+
* and errors for invalid entries.
|
|
5010
|
+
*/
|
|
5011
|
+
function coerceInputContext(form, inputContext) {
|
|
5012
|
+
const patches = [];
|
|
5013
|
+
const warnings = [];
|
|
5014
|
+
const errors = [];
|
|
5015
|
+
for (const [fieldId, rawValue] of Object.entries(inputContext)) {
|
|
5016
|
+
if (rawValue === null) continue;
|
|
5017
|
+
const result = coerceToFieldPatch(form, fieldId, rawValue);
|
|
5018
|
+
if (result.ok) {
|
|
5019
|
+
patches.push(result.patch);
|
|
5020
|
+
if ("warning" in result && result.warning) warnings.push(result.warning);
|
|
5021
|
+
} else errors.push(result.error);
|
|
5022
|
+
}
|
|
5023
|
+
return {
|
|
5024
|
+
patches,
|
|
5025
|
+
warnings,
|
|
5026
|
+
errors
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
//#endregion
|
|
5031
|
+
//#region src/engine/issueFiltering.ts
|
|
5032
|
+
/**
|
|
5033
|
+
* Extract field ID from an issue ref based on its scope.
|
|
5034
|
+
*
|
|
5035
|
+
* - field scope: ref IS the fieldId
|
|
5036
|
+
* - option/column/cell scope: ref is "fieldId.subId" — extract the prefix
|
|
5037
|
+
* - form/group scope: no field ID available
|
|
5038
|
+
*/
|
|
5039
|
+
function getFieldIdFromRef(ref, scope) {
|
|
5040
|
+
if (scope === "field") return ref;
|
|
5041
|
+
if (scope === "option" || scope === "column" || scope === "cell") {
|
|
5042
|
+
const dotIndex = ref.indexOf(".");
|
|
5043
|
+
return dotIndex > 0 ? ref.slice(0, dotIndex) : void 0;
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
/**
|
|
5047
|
+
* Get the parent group ID for a field from the form's ID index.
|
|
5048
|
+
*/
|
|
5049
|
+
function getGroupForField(form, fieldId) {
|
|
5050
|
+
const entry = form.idIndex.get(fieldId);
|
|
5051
|
+
if (!entry) return;
|
|
5052
|
+
if (entry.parentId) {
|
|
5053
|
+
if (form.idIndex.get(entry.parentId)?.nodeType === "group") return entry.parentId;
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
/**
|
|
5057
|
+
* Filter issues by order level.
|
|
5058
|
+
*
|
|
5059
|
+
* Only includes issues for fields at the current (lowest incomplete) order level.
|
|
5060
|
+
* Fields at higher order levels are deferred until all lower-order fields are complete.
|
|
5061
|
+
* If no order attributes are used, all issues pass through (all at order 0).
|
|
5062
|
+
*/
|
|
5063
|
+
function filterIssuesByOrder(issues, form) {
|
|
5064
|
+
const fieldOrderMap = /* @__PURE__ */ new Map();
|
|
5065
|
+
for (const group of form.schema.groups) {
|
|
5066
|
+
const groupOrder = group.order ?? 0;
|
|
5067
|
+
for (const field of group.children) fieldOrderMap.set(field.id, field.order ?? groupOrder);
|
|
5068
|
+
}
|
|
5069
|
+
const openOrderLevels = /* @__PURE__ */ new Set();
|
|
5070
|
+
for (const issue of issues) {
|
|
5071
|
+
const fieldId = getFieldIdFromRef(issue.ref, issue.scope);
|
|
5072
|
+
if (fieldId) {
|
|
5073
|
+
const order = fieldOrderMap.get(fieldId) ?? 0;
|
|
5074
|
+
openOrderLevels.add(order);
|
|
5075
|
+
} else if (issue.scope === "form") openOrderLevels.add(0);
|
|
5076
|
+
}
|
|
5077
|
+
if (openOrderLevels.size <= 1) return issues;
|
|
5078
|
+
const currentOrder = Math.min(...openOrderLevels);
|
|
5079
|
+
return issues.filter((issue) => {
|
|
5080
|
+
if (issue.scope === "form") return true;
|
|
5081
|
+
const fieldId = getFieldIdFromRef(issue.ref, issue.scope);
|
|
5082
|
+
if (!fieldId) return true;
|
|
5083
|
+
return (fieldOrderMap.get(fieldId) ?? 0) === currentOrder;
|
|
5084
|
+
});
|
|
5085
|
+
}
|
|
5086
|
+
/**
|
|
5087
|
+
* Filter issues based on maxFields and maxGroups limits.
|
|
5088
|
+
*
|
|
5089
|
+
* Issues are processed in priority order. An issue is included if:
|
|
5090
|
+
* - Adding it doesn't exceed the field limit (for field/option scoped issues)
|
|
5091
|
+
* - Adding it doesn't exceed the group limit
|
|
5092
|
+
*
|
|
5093
|
+
* Form-level issues are always included.
|
|
5094
|
+
*/
|
|
5095
|
+
function filterIssuesByScope(issues, form, maxFields, maxGroups) {
|
|
5096
|
+
if (maxFields === void 0 && maxGroups === void 0) return issues;
|
|
5097
|
+
const result = [];
|
|
5098
|
+
const seenFields = /* @__PURE__ */ new Set();
|
|
5099
|
+
const seenGroups = /* @__PURE__ */ new Set();
|
|
5100
|
+
for (const issue of issues) {
|
|
5101
|
+
if (issue.scope === "form") {
|
|
5102
|
+
result.push(issue);
|
|
5103
|
+
continue;
|
|
5104
|
+
}
|
|
5105
|
+
const fieldId = getFieldIdFromRef(issue.ref, issue.scope);
|
|
5106
|
+
const groupId = fieldId ? getGroupForField(form, fieldId) : void 0;
|
|
5107
|
+
if (maxFields !== void 0 && fieldId) {
|
|
5108
|
+
if (!seenFields.has(fieldId) && seenFields.size >= maxFields) continue;
|
|
5109
|
+
}
|
|
5110
|
+
if (maxGroups !== void 0 && groupId) {
|
|
5111
|
+
if (!seenGroups.has(groupId) && seenGroups.size >= maxGroups) continue;
|
|
5112
|
+
}
|
|
5113
|
+
result.push(issue);
|
|
5114
|
+
if (fieldId) seenFields.add(fieldId);
|
|
5115
|
+
if (groupId) seenGroups.add(groupId);
|
|
5116
|
+
}
|
|
5117
|
+
return result;
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
//#endregion
|
|
5121
|
+
//#region src/harness/prompts.ts
|
|
5122
|
+
/**
|
|
5123
|
+
* Agent Prompts - Centralized prompt definitions for the live agent.
|
|
5124
|
+
*
|
|
5125
|
+
* All hardcoded prompts are defined here for easy review, modification,
|
|
5126
|
+
* and future configurability. This file serves as the single source of
|
|
5127
|
+
* truth for agent behavior instructions.
|
|
5128
|
+
*/
|
|
5129
|
+
/**
|
|
5130
|
+
* Default system prompt for the live agent.
|
|
5131
|
+
*
|
|
5132
|
+
* This is the base instruction set that defines the agent's core behavior
|
|
5133
|
+
* for form filling. It emphasizes accuracy over completeness and prohibits
|
|
5134
|
+
* fabrication of data.
|
|
5135
|
+
*/
|
|
5136
|
+
const DEFAULT_SYSTEM_PROMPT = `# Form Instructions
|
|
5137
|
+
|
|
5138
|
+
Research and fill the form fields using all available tools. Focus on accuracy over completeness.
|
|
5139
|
+
|
|
5140
|
+
## Guidelines
|
|
5141
|
+
1. Address required fields first (severity: "required"), then optional fields (severity: "recommended")
|
|
5142
|
+
2. NEVER fabricate or guess information - only use data you can verify
|
|
5143
|
+
3. If you cannot find verifiable information, use skip_field with a reason
|
|
5144
|
+
|
|
5145
|
+
## Patch Format Examples
|
|
5146
|
+
|
|
5147
|
+
Use the fill_form tool with patches in these formats:
|
|
5148
|
+
|
|
5149
|
+
| Type | Example |
|
|
5150
|
+
|------|---------|
|
|
5151
|
+
| string | \`{ op: "set_string", fieldId: "name", value: "Acme Corp" }\` |
|
|
5152
|
+
| number | \`{ op: "set_number", fieldId: "age", value: 32 }\` |
|
|
5153
|
+
| string_list | \`{ op: "set_string_list", fieldId: "tags", value: ["ai", "ml"] }\` |
|
|
5154
|
+
| url | \`{ op: "set_url", fieldId: "website", value: "https://example.com" }\` |
|
|
5155
|
+
| url_list | \`{ op: "set_url_list", fieldId: "sources", value: ["https://a.com", "https://b.com"] }\` |
|
|
5156
|
+
| date | \`{ op: "set_date", fieldId: "event_date", value: "2024-06-15" }\` |
|
|
5157
|
+
| year | \`{ op: "set_year", fieldId: "founded", value: 2024 }\` |
|
|
5158
|
+
| single_select | \`{ op: "set_single_select", fieldId: "priority", value: "high" }\` |
|
|
5159
|
+
| multi_select | \`{ op: "set_multi_select", fieldId: "categories", value: ["frontend", "backend"] }\` |
|
|
5160
|
+
| checkboxes | \`{ op: "set_checkboxes", fieldId: "tasks", value: { "task1": "done", "task2": "todo" } }\` |
|
|
5161
|
+
| table | \`{ op: "set_table", fieldId: "team", value: [{ "name": "Alice", "role": "Engineer" }] }\` |
|
|
5162
|
+
|
|
5163
|
+
## Incremental Operations (for collections)
|
|
5164
|
+
|
|
5165
|
+
Use these to add/remove items without replacing the entire collection:
|
|
5166
|
+
|
|
5167
|
+
| Type | Example |
|
|
5168
|
+
|------|---------|
|
|
5169
|
+
| append_table | \`{ op: "append_table", fieldId: "team", value: [{ "name": "Bob", "role": "PM" }] }\` |
|
|
5170
|
+
| append_string_list | \`{ op: "append_string_list", fieldId: "tags", value: ["new_tag"] }\` |
|
|
5171
|
+
| append_url_list | \`{ op: "append_url_list", fieldId: "sources", value: ["https://new.com"] }\` |
|
|
5172
|
+
| delete_table | \`{ op: "delete_table", fieldId: "team", value: 0 }\` (0-based row index) |
|
|
5173
|
+
| delete_string_list | \`{ op: "delete_string_list", fieldId: "tags", value: 0 }\` (0-based item index) |
|
|
5174
|
+
| delete_url_list | \`{ op: "delete_url_list", fieldId: "sources", value: 0 }\` (0-based item index) |
|
|
5175
|
+
|
|
5176
|
+
## Important: checkboxes vs multi_select
|
|
5177
|
+
|
|
5178
|
+
These two types look similar but have DIFFERENT value formats:
|
|
5179
|
+
|
|
5180
|
+
- **multi_select** → array of option IDs: \`["opt1", "opt2"]\`
|
|
5181
|
+
- **checkboxes** → object mapping IDs to states: \`{ "opt1": "done", "opt2": "todo" }\`
|
|
5182
|
+
|
|
5183
|
+
**Checkbox states by mode:**
|
|
5184
|
+
- Mode "simple": \`"done"\` or \`"todo"\`
|
|
5185
|
+
- Mode "multi": \`"done"\`, \`"todo"\`, \`"incomplete"\`, \`"active"\`, or \`"na"\`
|
|
5186
|
+
- Mode "explicit": \`"yes"\` or \`"no"\` (if unknown, use abort_field)
|
|
5187
|
+
|
|
5188
|
+
**WRONG:** \`{ op: "set_checkboxes", value: ["task1", "task2"] }\`
|
|
5189
|
+
**RIGHT:** \`{ op: "set_checkboxes", value: { "task1": "done", "task2": "done" } }\`
|
|
5190
|
+
|
|
5191
|
+
## Skipping Fields
|
|
5192
|
+
|
|
5193
|
+
If you cannot find verifiable information:
|
|
5194
|
+
\`{ op: "skip_field", fieldId: "...", reason: "Could not find verified data" }\`
|
|
5195
|
+
`;
|
|
5196
|
+
/**
|
|
5197
|
+
* Web search instructions appended when web search tools are available.
|
|
5198
|
+
*
|
|
5199
|
+
* These instructions enforce that the agent must verify all information
|
|
5200
|
+
* through web search before filling fields.
|
|
5201
|
+
*/
|
|
5202
|
+
const WEB_SEARCH_INSTRUCTIONS = `# Web Search
|
|
5203
|
+
You have access to web search tools. You MUST use them to verify ALL information before filling fields.
|
|
5204
|
+
|
|
5205
|
+
Guidelines:
|
|
5206
|
+
1. Search for official sources (company websites, Crunchbase, LinkedIn, press releases)
|
|
5207
|
+
2. Cross-reference information across multiple sources when possible
|
|
5208
|
+
3. Only fill fields with data you found and verified through search
|
|
5209
|
+
4. If a search returns no results or uncertain information, use skip_field with a reason explaining what you searched for
|
|
5210
|
+
5. NEVER fill fields with guessed or assumed information
|
|
5211
|
+
`;
|
|
5212
|
+
/**
|
|
5213
|
+
* Header for the issues section in the context prompt.
|
|
5214
|
+
*/
|
|
5215
|
+
const ISSUES_HEADER = "# Current Form Issues";
|
|
5216
|
+
/**
|
|
5217
|
+
* Template for the issues intro text.
|
|
5218
|
+
* @param issueCount - Actual number of issues shown
|
|
5219
|
+
*/
|
|
5220
|
+
function getIssuesIntro(issueCount) {
|
|
5221
|
+
return `You need to address ${issueCount} issue${issueCount === 1 ? "" : "s"}. Here are the current issues:`;
|
|
5222
|
+
}
|
|
5223
|
+
/**
|
|
5224
|
+
* Patch format examples by field kind.
|
|
5225
|
+
*
|
|
5226
|
+
* This is the single source of truth for patch format documentation.
|
|
5227
|
+
* Used in PATCH_FORMAT_INSTRUCTIONS and rejection feedback hints.
|
|
5228
|
+
*/
|
|
5229
|
+
const PATCH_FORMATS = {
|
|
5230
|
+
string: "{ op: \"set_string\", fieldId: \"...\", value: \"text here\" }",
|
|
5231
|
+
number: "{ op: \"set_number\", fieldId: \"...\", value: 42 }",
|
|
5232
|
+
string_list: "{ op: \"set_string_list\", fieldId: \"...\", value: [\"item1\", \"item2\"] }",
|
|
5233
|
+
single_select: "{ op: \"set_single_select\", fieldId: \"...\", value: \"option_id\" }",
|
|
5234
|
+
multi_select: "{ op: \"set_multi_select\", fieldId: \"...\", value: [\"opt1\", \"opt2\"] }",
|
|
5235
|
+
checkboxes: "{ op: \"set_checkboxes\", fieldId: \"...\", value: { \"opt1\": \"done\", \"opt2\": \"todo\" } }",
|
|
5236
|
+
url: "{ op: \"set_url\", fieldId: \"...\", value: \"https://example.com\" }",
|
|
5237
|
+
url_list: "{ op: \"set_url_list\", fieldId: \"...\", value: [\"https://a.com\", \"https://b.com\"] }",
|
|
5238
|
+
date: "{ op: \"set_date\", fieldId: \"...\", value: \"2024-06-15\" }",
|
|
5239
|
+
year: "{ op: \"set_year\", fieldId: \"...\", value: 2024 }",
|
|
5240
|
+
table: "{ op: \"set_table\", fieldId: \"...\", value: [{ col1: \"val1\", col2: \"val2\" }] }",
|
|
5241
|
+
append_table: "{ op: \"append_table\", fieldId: \"...\", value: [{ col1: \"val1\", col2: \"val2\" }] }",
|
|
5242
|
+
append_string_list: "{ op: \"append_string_list\", fieldId: \"...\", value: [\"new_item\"] }",
|
|
5243
|
+
append_url_list: "{ op: \"append_url_list\", fieldId: \"...\", value: [\"https://new.com\"] }",
|
|
5244
|
+
delete_table: "{ op: \"delete_table\", fieldId: \"...\", value: 0 }",
|
|
5245
|
+
delete_string_list: "{ op: \"delete_string_list\", fieldId: \"...\", value: 0 }",
|
|
5246
|
+
delete_url_list: "{ op: \"delete_url_list\", fieldId: \"...\", value: 0 }"
|
|
5247
|
+
};
|
|
5248
|
+
/**
|
|
5249
|
+
* Get the correct patch format for a field kind.
|
|
5250
|
+
*
|
|
5251
|
+
* @param fieldKind - The field kind (e.g., "table", "string")
|
|
5252
|
+
* @param options - Optional configuration for the hint
|
|
5253
|
+
* @returns The patch format example string
|
|
5254
|
+
*/
|
|
5255
|
+
function getPatchFormatHint(fieldKind, fieldIdOrOptions, columnIds) {
|
|
5256
|
+
let options = {};
|
|
5257
|
+
if (typeof fieldIdOrOptions === "string") options = {
|
|
5258
|
+
fieldId: fieldIdOrOptions,
|
|
5259
|
+
columnIds
|
|
5260
|
+
};
|
|
5261
|
+
else if (fieldIdOrOptions) options = fieldIdOrOptions;
|
|
5262
|
+
let format = PATCH_FORMATS[fieldKind];
|
|
5263
|
+
if (!format) return `Use the correct set_${fieldKind} operation for this field type.`;
|
|
5264
|
+
if (options.fieldId) format = format.replace("fieldId: \"...\"", `fieldId: "${options.fieldId}"`);
|
|
5265
|
+
if (fieldKind === "checkboxes") {
|
|
5266
|
+
const mode = options.checkboxMode ?? "multi";
|
|
5267
|
+
const optIds = options.optionIds ?? ["opt1", "opt2"];
|
|
5268
|
+
const [state1, state2] = {
|
|
5269
|
+
simple: ["done", "todo"],
|
|
5270
|
+
multi: ["done", "todo"],
|
|
5271
|
+
explicit: ["yes", "no"]
|
|
5272
|
+
}[mode] ?? ["done", "todo"];
|
|
5273
|
+
const valueExample = optIds.length >= 2 ? `{ "${optIds[0]}": "${state1}", "${optIds[1]}": "${state2}" }` : optIds.length === 1 ? `{ "${optIds[0]}": "${state1}" }` : `{ "opt1": "${state1}", "opt2": "${state2}" }`;
|
|
5274
|
+
format = format.replace("{ \"opt1\": \"done\", \"opt2\": \"todo\" }", valueExample);
|
|
5275
|
+
}
|
|
5276
|
+
if ((fieldKind === "table" || fieldKind === "append_table") && options.columnIds && options.columnIds.length > 0) {
|
|
5277
|
+
const colExample = options.columnIds.map((id) => `"${id}": "..."`).join(", ");
|
|
5278
|
+
format = format.replace("{ col1: \"val1\", col2: \"val2\" }", `{ ${colExample} }`);
|
|
5279
|
+
}
|
|
5280
|
+
return format;
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Instructions section for the context prompt.
|
|
5284
|
+
*
|
|
5285
|
+
* This explains the patch format for each field kind.
|
|
5286
|
+
* Generated from PATCH_FORMATS to ensure consistency.
|
|
5287
|
+
*/
|
|
5288
|
+
const PATCH_FORMAT_INSTRUCTIONS = `# Instructions
|
|
5289
|
+
|
|
5290
|
+
Use the fill_form tool to submit patches for the fields above.
|
|
5291
|
+
Each patch should match the field kind:
|
|
5292
|
+
${Object.entries(PATCH_FORMATS).map(([kind, format]) => `- ${kind}: ${format}`).join("\n")}
|
|
5293
|
+
|
|
5294
|
+
For table fields, use the column IDs shown in the field schema. Each row is an object with column ID keys.
|
|
5295
|
+
|
|
5296
|
+
If you cannot find verifiable information for a field, skip it:
|
|
5297
|
+
- skip: { op: "skip_field", fieldId: "...", reason: "Information not available" }`;
|
|
5298
|
+
/**
|
|
5299
|
+
* Simplified general instructions for use with inline field instructions.
|
|
5300
|
+
*
|
|
5301
|
+
* When inline field instructions are shown after each issue, we only need
|
|
5302
|
+
* general guidance about using the fill_form tool.
|
|
5303
|
+
*/
|
|
5304
|
+
const GENERAL_INSTRUCTIONS = `# General Instructions
|
|
5305
|
+
|
|
5306
|
+
Use the fill_form tool to submit patches for the fields above.
|
|
5307
|
+
For table fields, each row is an object with column ID keys.`;
|
|
5308
|
+
/**
|
|
5309
|
+
* Section headers used when building the composed system prompt.
|
|
5310
|
+
*/
|
|
5311
|
+
const SECTION_HEADERS = {
|
|
5312
|
+
formInstructions: "# Form Instructions",
|
|
5313
|
+
roleInstructions: (role) => `# Instructions for ${role} role`,
|
|
5314
|
+
roleGuidance: "# Role guidance",
|
|
5315
|
+
fieldInstructions: "# Field-specific instructions",
|
|
5316
|
+
additionalContext: "# Additional Context"
|
|
5317
|
+
};
|
|
5318
|
+
|
|
5319
|
+
//#endregion
|
|
5320
|
+
export { DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN as $, tryParseSentinelResponse as A, isParseError as At, isTagNode as B, isFormComplete as C, MarkformPatchError as Ct, detectSyntaxStyle as D, isConfigError as Dt, serializeReport as E, isAbortError as Et, getBooleanAttr as F, DEFAULT_MAX_PARALLEL_AGENTS as G, AGENT_ROLE as H, getNumberAttr as I, DEFAULT_MAX_STEPS_PER_TURN as J, DEFAULT_MAX_PATCHES_PER_TURN as K, getStringArrayAttr as L, extractFenceValue as M, isRetryableError as Mt, extractOptionItems as N, isValidationError as Nt, preprocessCommentSyntax as O, isLlmError as Ot, extractTableContent as P, wrapApiError as Pt, DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN as Q, getStringAttr as R, computeStructureSummary as S, MarkformParseError as St, serializeRawMarkdown as T, ParseError as Tt, DEFAULT_FORMS_DIR as U, parseOptionText as V, DEFAULT_MAX_ISSUES_PER_TURN as W, DEFAULT_PORT as X, DEFAULT_MAX_TURNS as Y, DEFAULT_PRIORITY as Z, inspect as _, parseModelIdForDisplay as _t, WEB_SEARCH_INSTRUCTIONS as a, deriveExportPath as at, computeFormState as b, MarkformError as bt, filterIssuesByOrder as c, deriveSchemaPath as ct, coerceInputContext as d, transformHarnessConfigToTs as dt, DEFAULT_ROLES as et, coerceToFieldPatch as f, SUGGESTED_LLMS as ft, getFieldsForRoles as g, hasWebSearchSupport as gt, getAllFields as h, getWebSearchConfig as ht, SECTION_HEADERS as i, USER_ROLE as it, CHECKBOX_MARKERS as j, isPatchError as jt, validateSyntaxConsistency as k, isMarkformError as kt, filterIssuesByScope as l, detectFileType as lt, applyPatches as m, formatSuggestedLlms as mt, GENERAL_INSTRUCTIONS as n, MAX_FORMS_IN_MENU as nt, getIssuesIntro as o, deriveFillRecordPath as ot, findFieldById as p, WEB_SEARCH_CONFIG as pt, DEFAULT_MAX_RETRIES as q, ISSUES_HEADER as r, REPORT_EXTENSION as rt, getPatchFormatHint as s, deriveReportPath as st, DEFAULT_SYSTEM_PROMPT as t, DEFAULT_ROLE_INSTRUCTIONS as tt, getFieldIdFromRef as u, parseRolesFlag as ut, validate as v, MarkformAbortError as vt, serializeForm as w, MarkformValidationError as wt, computeProgressSummary as x, MarkformLlmError as xt, computeAllSummaries as y, MarkformConfigError as yt, getValidateAttr as z };
|
|
5321
|
+
//# sourceMappingURL=prompts-BCnYaH4_.mjs.map
|