@primmel/primmel 1.19.0 → 1.20.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.
@@ -9169,6 +9169,227 @@ function parseInstances(block) {
9169
9169
  }
9170
9170
  return out;
9171
9171
  }
9172
+ function parseProgramArgs(block) {
9173
+ const out = {};
9174
+ const t = tokenize(block);
9175
+ let i = 0;
9176
+ while (i < t.length) {
9177
+ const key = stripColon(t[i++]);
9178
+ if (i >= t.length) {
9179
+ break;
9180
+ }
9181
+ if (t[i] === ":") {
9182
+ i++;
9183
+ }
9184
+ if (i < t.length) {
9185
+ const read = readValueToken(t, i);
9186
+ out[key] = stripWrapping(read.text);
9187
+ i = read.next;
9188
+ }
9189
+ }
9190
+ return out;
9191
+ }
9192
+ function parseVerify(block) {
9193
+ const v = { read: "", tolerance: "" };
9194
+ const t = tokenize(block);
9195
+ let i = 0;
9196
+ while (i < t.length) {
9197
+ const cmd = t[i++];
9198
+ if (i >= t.length) {
9199
+ break;
9200
+ }
9201
+ if (cmd === "read") {
9202
+ v.read = stripWrapping(t[i++]);
9203
+ } else if (cmd === "tolerance") {
9204
+ v.tolerance = stripWrapping(t[i++]);
9205
+ } else {
9206
+ unwrapBlock(t[i++]);
9207
+ }
9208
+ }
9209
+ return v;
9210
+ }
9211
+ function parseProgramBody(block, entryKeyword, makeEntry, parseEntryFacets, errCtx) {
9212
+ const program = {
9213
+ description: "",
9214
+ entries: [],
9215
+ sourceRefs: []
9216
+ };
9217
+ const t = tokenize(block);
9218
+ let i = 0;
9219
+ while (i < t.length) {
9220
+ const command = t[i++];
9221
+ if (i >= t.length) {
9222
+ throw new Error(
9223
+ `Parsing error: ${errCtx}: Expecting value for ${command}`
9224
+ );
9225
+ }
9226
+ if (command === "description") {
9227
+ program.description = stripWrapping(t[i++]);
9228
+ } else if (command === "source") {
9229
+ program.sourceRefs.push(readSource(unwrapBlock(t[i++])));
9230
+ } else if (command === "ref") {
9231
+ const rr = parseRef(t, i, stripWrapping, unwrapBlock);
9232
+ foldRefIntoLegacy(program, rr.ref);
9233
+ i = rr.next;
9234
+ } else if (command === entryKeyword) {
9235
+ const entry = makeEntry();
9236
+ let value = t[i++];
9237
+ if (!value.startsWith("{")) {
9238
+ entry.order = Number(stripWrapping(value));
9239
+ value = i < t.length && t[i].startsWith("{") ? t[i++] : "";
9240
+ }
9241
+ if (value.startsWith("{")) {
9242
+ parseEntryFacets(unwrapBlock(value), entry);
9243
+ }
9244
+ program.entries.push(entry);
9245
+ } else {
9246
+ i = skipUnknownValue(t, i, command);
9247
+ }
9248
+ }
9249
+ return program;
9250
+ }
9251
+ function parsePreparation(block, id) {
9252
+ return parseProgramBody(
9253
+ block,
9254
+ "step",
9255
+ () => ({
9256
+ order: null,
9257
+ action: "",
9258
+ drive: "",
9259
+ args: {},
9260
+ hold: "",
9261
+ verify: null
9262
+ }),
9263
+ (stepBlock, step) => {
9264
+ forEachEntry(
9265
+ stepBlock,
9266
+ (facet, value) => {
9267
+ if (facet === "action") {
9268
+ step.action = unwrapped(value);
9269
+ } else if (facet === "drive") {
9270
+ step.drive = stripWrapping(value());
9271
+ } else if (facet === "args") {
9272
+ step.args = parseProgramArgs(unwrapBlock(value()));
9273
+ } else if (facet === "hold") {
9274
+ step.hold = stripWrapping(value());
9275
+ } else if (facet === "verify") {
9276
+ step.verify = parseVerify(unwrapBlock(value()));
9277
+ } else {
9278
+ return false;
9279
+ }
9280
+ return true;
9281
+ },
9282
+ { construct: "conformance_test.preparation", id }
9283
+ );
9284
+ },
9285
+ `conformance_test.preparation. ID ${id}`
9286
+ );
9287
+ }
9288
+ function parseStimulus(block, id) {
9289
+ return parseProgramBody(
9290
+ block,
9291
+ "point",
9292
+ () => ({
9293
+ order: null,
9294
+ drive: "",
9295
+ args: {},
9296
+ hold: "",
9297
+ freshWithin: "",
9298
+ acceptance: ""
9299
+ }),
9300
+ (pointBlock, point) => {
9301
+ forEachEntry(
9302
+ pointBlock,
9303
+ (facet, value) => {
9304
+ if (facet === "drive") {
9305
+ point.drive = stripWrapping(value());
9306
+ } else if (facet === "args") {
9307
+ point.args = parseProgramArgs(unwrapBlock(value()));
9308
+ } else if (facet === "hold") {
9309
+ point.hold = stripWrapping(value());
9310
+ } else if (facet === "fresh_within") {
9311
+ point.freshWithin = stripWrapping(value());
9312
+ } else if (facet === "acceptance") {
9313
+ point.acceptance = stripWrapping(value());
9314
+ } else {
9315
+ return false;
9316
+ }
9317
+ return true;
9318
+ },
9319
+ { construct: "conformance_test.stimulus", id }
9320
+ );
9321
+ },
9322
+ `conformance_test.stimulus. ID ${id}`
9323
+ );
9324
+ }
9325
+ function dumpProgramArgs(args2) {
9326
+ const keys = Object.keys(args2);
9327
+ if (keys.length === 0) {
9328
+ return "";
9329
+ }
9330
+ return " args { " + keys.map((k) => k + ': "' + escapeString(args2[k]) + '"').join(" ") + " }";
9331
+ }
9332
+ function dumpPreparation(program) {
9333
+ let out = " preparation {\n";
9334
+ if (program.description !== "") {
9335
+ out += ' description "' + escapeString(program.description) + '"\n';
9336
+ }
9337
+ for (const step of program.entries) {
9338
+ out += " step" + (step.order !== null ? " " + step.order : "") + " {";
9339
+ if (step.action !== "") {
9340
+ out += ' action "' + escapeString(step.action) + '"';
9341
+ }
9342
+ if (step.drive !== "") {
9343
+ out += " drive " + dumpBareSafe(step.drive);
9344
+ }
9345
+ out += dumpProgramArgs(step.args);
9346
+ if (step.hold !== "") {
9347
+ out += " hold " + dumpBareSafe(step.hold);
9348
+ }
9349
+ if (step.verify) {
9350
+ out += " verify {";
9351
+ if (step.verify.read !== "") {
9352
+ out += " read " + dumpBareSafe(step.verify.read);
9353
+ }
9354
+ if (step.verify.tolerance !== "") {
9355
+ out += " tolerance " + dumpBareSafe(step.verify.tolerance);
9356
+ }
9357
+ out += " }";
9358
+ }
9359
+ out += " }\n";
9360
+ }
9361
+ for (const src of program.sourceRefs) {
9362
+ out += dumpSourceRefAsRef(src, " ", escapeString);
9363
+ }
9364
+ return out + " }\n";
9365
+ }
9366
+ function dumpStimulus(program) {
9367
+ let out = " stimulus {\n";
9368
+ if (program.description !== "") {
9369
+ out += ' description "' + escapeString(program.description) + '"\n';
9370
+ }
9371
+ for (const point of program.entries) {
9372
+ out += " point" + (point.order !== null ? " " + point.order : "") + " {";
9373
+ if (point.drive !== "") {
9374
+ out += " drive " + dumpBareSafe(point.drive);
9375
+ }
9376
+ out += dumpProgramArgs(point.args);
9377
+ if (point.hold !== "") {
9378
+ out += " hold " + dumpBareSafe(point.hold);
9379
+ }
9380
+ if (point.freshWithin !== "") {
9381
+ out += " fresh_within " + dumpBareSafe(point.freshWithin);
9382
+ }
9383
+ if (point.acceptance !== "") {
9384
+ out += " acceptance " + dumpBareSafe(point.acceptance);
9385
+ }
9386
+ out += " }\n";
9387
+ }
9388
+ for (const src of program.sourceRefs) {
9389
+ out += dumpSourceRefAsRef(src, " ", escapeString);
9390
+ }
9391
+ return out + " }\n";
9392
+ }
9172
9393
  var parseConformanceTest = function(id, data) {
9173
9394
  const result = {
9174
9395
  id,
@@ -9180,6 +9401,8 @@ var parseConformanceTest = function(id, data) {
9180
9401
  bindsTo: [],
9181
9402
  applicability: [],
9182
9403
  procedure: [],
9404
+ preparation: null,
9405
+ stimulus: null,
9183
9406
  measurements: [],
9184
9407
  kind: "",
9185
9408
  obligation: "",
@@ -9302,6 +9525,10 @@ var parseConformanceTest = function(id, data) {
9302
9525
  }
9303
9526
  } else if (keyword === "procedure_steps") {
9304
9527
  result.procedureSteps = readStringList(value());
9528
+ } else if (keyword === "preparation") {
9529
+ result.preparation = parsePreparation(unwrapBlock(value()), id);
9530
+ } else if (keyword === "stimulus") {
9531
+ result.stimulus = parseStimulus(unwrapBlock(value()), id);
9305
9532
  } else if (keyword === "kind") {
9306
9533
  result.kind = stripWrapping(value());
9307
9534
  } else if (keyword === "obligation") {
@@ -9432,6 +9659,12 @@ var dumpConformanceTest = function(ct) {
9432
9659
  if (ct.procedureSteps && ct.procedureSteps.length > 0) {
9433
9660
  out += " procedure_steps { " + ct.procedureSteps.join(" ") + " }\n";
9434
9661
  }
9662
+ if (ct.preparation) {
9663
+ out += dumpPreparation(ct.preparation);
9664
+ }
9665
+ if (ct.stimulus) {
9666
+ out += dumpStimulus(ct.stimulus);
9667
+ }
9435
9668
  if (ct.measurements.length > 0) {
9436
9669
  out += " validate_measurement {\n";
9437
9670
  for (const m of ct.measurements) {
@@ -22845,6 +23078,37 @@ var CHECK_RULES = [
22845
23078
  "normal",
22846
23079
  "smart architecture-gaps-2026-07.md E10, data/schemas/test-sequences.yaml"
22847
23080
  ),
23081
+ // ── the executable test programs (smart TODO.twin-demo/03 — the
23082
+ // model-drive bindings) ──
23083
+ // The conformance test's preparation/stimulus facets are the
23084
+ // machine-executable half of the test declaration. C147 is the
23085
+ // program shape: entry orders are positive integers unique in the
23086
+ // program; a preparation step carries action AND/OR drive; a verify
23087
+ // block carries read AND tolerance; a stimulus point carries a drive
23088
+ // and an acceptance reference. C148 is the duration discipline: hold
23089
+ // and fresh_within parse as duration windows (the serve contract's
23090
+ // vocabulary), and a stimulus point declares fresh_within (§14.12's
23091
+ // no-stale-semantics doctrine). Reference RESOLUTION (the drive
23092
+ // operation names against the twin kind's declared vocabulary, the
23093
+ // acceptance/tolerance refs against requirement nodes) is the
23094
+ // consumer-side conformance leg's job — the kernel checks
23095
+ // syntax/shape only (the C92/C93 vs R39 split).
23096
+ R(
23097
+ "C147",
23098
+ "test-program-shape",
23099
+ "base",
23100
+ "error",
23101
+ "normal",
23102
+ "smart TODO.twin-demo/03 (the model-drive bindings)"
23103
+ ),
23104
+ R(
23105
+ "C148",
23106
+ "test-program-duration",
23107
+ "base",
23108
+ "error",
23109
+ "normal",
23110
+ "smart TODO.twin-demo/03 (the model-drive bindings)"
23111
+ ),
22848
23112
  // ── the per-test evaluation-formula traces (smart gap-close E11,
22849
23113
  // analysis/architecture-gaps-2026-07.md; the smart contract
22850
23114
  // data/schemas/formulas-used.yaml) ──
@@ -26803,6 +27067,97 @@ function checkPackage(dir, options = {}) {
26803
27067
  }
26804
27068
  }
26805
27069
  }
27070
+ {
27071
+ for (const ct of standard.conformanceTests ?? []) {
27072
+ const prep = ct.preparation;
27073
+ if (prep) {
27074
+ const orders = /* @__PURE__ */ new Set();
27075
+ for (const step of prep.entries) {
27076
+ const at = step.order !== null ? `preparation step ${step.order}` : "a preparation step";
27077
+ if (step.order === null || !Number.isInteger(step.order) || step.order < 1) {
27078
+ err(
27079
+ "C147",
27080
+ `conformance_test ${ct.id}: ${at} declares no positive-integer order (got ${step.order === null ? "none" : step.order}) \u2014 the step order is a positive integer (test-program-shape)`
27081
+ );
27082
+ } else if (orders.has(step.order)) {
27083
+ err(
27084
+ "C147",
27085
+ `conformance_test ${ct.id}: preparation step order ${step.order} is declared twice \u2014 the order is unique within the program (test-program-shape)`
27086
+ );
27087
+ } else {
27088
+ orders.add(step.order);
27089
+ }
27090
+ if (step.action === "" && step.drive === "") {
27091
+ err(
27092
+ "C147",
27093
+ `conformance_test ${ct.id}: ${at} declares neither action nor drive \u2014 a preparation step carries a prose action AND/OR a drive reference (test-program-shape)`
27094
+ );
27095
+ }
27096
+ if (step.verify && (step.verify.read === "" || step.verify.tolerance === "")) {
27097
+ err(
27098
+ "C147",
27099
+ `conformance_test ${ct.id}: ${at} declares a verify block without ${step.verify.read === "" ? "read" : "tolerance"} \u2014 the verification names what to read AND the tolerance reference (test-program-shape)`
27100
+ );
27101
+ }
27102
+ if (step.hold !== "" && parseFreshnessWindow(step.hold) === null) {
27103
+ err(
27104
+ "C148",
27105
+ `conformance_test ${ct.id}: ${at} hold "${step.hold}" is not a parseable duration (shorthand 500ms/5s/1min/1h/1d or ISO 8601 with fixed-length components, e.g. PT30M) (test-program-duration)`
27106
+ );
27107
+ }
27108
+ }
27109
+ }
27110
+ const stim = ct.stimulus;
27111
+ if (stim) {
27112
+ const orders = /* @__PURE__ */ new Set();
27113
+ for (const point of stim.entries) {
27114
+ const at = point.order !== null ? `stimulus point ${point.order}` : "a stimulus point";
27115
+ if (point.order === null || !Number.isInteger(point.order) || point.order < 1) {
27116
+ err(
27117
+ "C147",
27118
+ `conformance_test ${ct.id}: ${at} declares no positive-integer order (got ${point.order === null ? "none" : point.order}) \u2014 the point order is a positive integer (test-program-shape)`
27119
+ );
27120
+ } else if (orders.has(point.order)) {
27121
+ err(
27122
+ "C147",
27123
+ `conformance_test ${ct.id}: stimulus point order ${point.order} is declared twice \u2014 the order is unique within the program (test-program-shape)`
27124
+ );
27125
+ } else {
27126
+ orders.add(point.order);
27127
+ }
27128
+ if (point.drive === "") {
27129
+ err(
27130
+ "C147",
27131
+ `conformance_test ${ct.id}: ${at} declares no drive \u2014 a stimulus point names the drive operation (test-program-shape)`
27132
+ );
27133
+ }
27134
+ if (point.acceptance === "") {
27135
+ err(
27136
+ "C147",
27137
+ `conformance_test ${ct.id}: ${at} declares no acceptance \u2014 a stimulus point names the MPE/requirement node it is judged against (test-program-shape)`
27138
+ );
27139
+ }
27140
+ if (point.hold !== "" && parseFreshnessWindow(point.hold) === null) {
27141
+ err(
27142
+ "C148",
27143
+ `conformance_test ${ct.id}: ${at} hold "${point.hold}" is not a parseable duration (shorthand 500ms/5s/1min/1h/1d or ISO 8601 with fixed-length components, e.g. PT30M) (test-program-duration)`
27144
+ );
27145
+ }
27146
+ if (point.freshWithin === "") {
27147
+ err(
27148
+ "C148",
27149
+ `conformance_test ${ct.id}: ${at} declares no fresh_within \u2014 a measurement point's read carries its freshness bound (\xA714.12: no stale semantics) (test-program-duration)`
27150
+ );
27151
+ } else if (parseFreshnessWindow(point.freshWithin) === null) {
27152
+ err(
27153
+ "C148",
27154
+ `conformance_test ${ct.id}: ${at} fresh_within "${point.freshWithin}" is not a parseable freshness window (shorthand 500ms/5s/1min/1h/1d or ISO 8601 with fixed-length components, e.g. PT5S) (test-program-duration)`
27155
+ );
27156
+ }
27157
+ }
27158
+ }
27159
+ }
27160
+ }
26806
27161
  {
26807
27162
  for (const fu of standard.formulasUsed ?? []) {
26808
27163
  if (stripWrapping(fu.id) === "") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primmel/primmel",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Primmel — parser, types, and serializer for the Primmel modelling language (successor to MMEL)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",