@gate-forge/core 0.7.1 → 0.9.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.
- package/dist/classifier/bind.d.ts +2 -0
- package/dist/classifier/bind.d.ts.map +1 -1
- package/dist/classifier/bind.js +8 -0
- package/dist/classifier/bind.js.map +1 -1
- package/dist/classifier/classify.d.ts +2 -0
- package/dist/classifier/classify.d.ts.map +1 -1
- package/dist/classifier/classify.js +127 -2
- package/dist/classifier/classify.js.map +1 -1
- package/dist/classifier/index.d.ts +2 -2
- package/dist/classifier/index.d.ts.map +1 -1
- package/dist/classifier/index.js +1 -1
- package/dist/classifier/index.js.map +1 -1
- package/dist/classifier/schema.d.ts +54 -0
- package/dist/classifier/schema.d.ts.map +1 -1
- package/dist/classifier/schema.js +17 -0
- package/dist/classifier/schema.js.map +1 -1
- package/dist/config/index.d.ts +186 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +327 -1
- package/dist/config/index.js.map +1 -1
- package/dist/graph/schema.d.ts +34 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/index.d.ts +80 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -6
- package/dist/index.js.map +1 -1
- package/dist/mapping/resolve.d.ts +32 -1
- package/dist/mapping/resolve.d.ts.map +1 -1
- package/dist/mapping/resolve.js +297 -65
- package/dist/mapping/resolve.js.map +1 -1
- package/dist/policy/evaluate.d.ts +30 -0
- package/dist/policy/evaluate.d.ts.map +1 -1
- package/dist/policy/evaluate.js +22 -12
- package/dist/policy/evaluate.js.map +1 -1
- package/dist/quarantine/index.d.ts +82 -0
- package/dist/quarantine/index.d.ts.map +1 -0
- package/dist/quarantine/index.js +194 -0
- package/dist/quarantine/index.js.map +1 -0
- package/dist/receipt/index.d.ts +65 -1
- package/dist/receipt/index.d.ts.map +1 -1
- package/dist/receipt/index.js +115 -1
- package/dist/receipt/index.js.map +1 -1
- package/dist/report/human-message.d.ts +33 -0
- package/dist/report/human-message.d.ts.map +1 -0
- package/dist/report/human-message.js +33 -0
- package/dist/report/human-message.js.map +1 -0
- package/dist/report/index.d.ts +183 -9
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +317 -23
- package/dist/report/index.js.map +1 -1
- package/dist/schemas/adoption.d.ts +2 -0
- package/dist/schemas/adoption.d.ts.map +1 -1
- package/dist/schemas/adoption.js +6 -0
- package/dist/schemas/adoption.js.map +1 -1
- package/dist/schemas/behavior-catalog.d.ts +12 -0
- package/dist/schemas/behavior-catalog.d.ts.map +1 -1
- package/dist/schemas/behavior-evidence.d.ts +40 -0
- package/dist/schemas/behavior-evidence.d.ts.map +1 -1
- package/dist/schemas/behavior-evidence.js +9 -0
- package/dist/schemas/behavior-evidence.js.map +1 -1
- package/dist/schemas/behavior-policy.d.ts +26 -0
- package/dist/schemas/behavior-policy.d.ts.map +1 -1
- package/dist/schemas/behavior-policy.js +37 -1
- package/dist/schemas/behavior-policy.js.map +1 -1
- package/dist/schemas/execution-result.d.ts +17 -5
- package/dist/schemas/execution-result.d.ts.map +1 -1
- package/dist/schemas/execution-result.js +57 -2
- package/dist/schemas/execution-result.js.map +1 -1
- package/dist/schemas/gate-receipt.d.ts +25 -2
- package/dist/schemas/gate-receipt.d.ts.map +1 -1
- package/dist/schemas/gate-receipt.js +181 -2
- package/dist/schemas/gate-receipt.js.map +1 -1
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/policy.d.ts +19 -0
- package/dist/schemas/policy.d.ts.map +1 -1
- package/dist/schemas/policy.js +36 -0
- package/dist/schemas/policy.js.map +1 -1
- package/dist/schemas/quarantine.d.ts +25 -0
- package/dist/schemas/quarantine.d.ts.map +1 -0
- package/dist/schemas/quarantine.js +34 -0
- package/dist/schemas/quarantine.js.map +1 -0
- package/dist/schemas/queue-observer.d.ts +195 -0
- package/dist/schemas/queue-observer.d.ts.map +1 -0
- package/dist/schemas/queue-observer.js +190 -0
- package/dist/schemas/queue-observer.js.map +1 -0
- package/dist/schemas/run-record.d.ts +57 -0
- package/dist/schemas/run-record.d.ts.map +1 -0
- package/dist/schemas/run-record.js +108 -0
- package/dist/schemas/run-record.js.map +1 -0
- package/dist/schemas/runtime-config.d.ts +67 -0
- package/dist/schemas/runtime-config.d.ts.map +1 -1
- package/dist/schemas/runtime-config.js +75 -1
- package/dist/schemas/runtime-config.js.map +1 -1
- package/dist/schemas/signature-profile.d.ts +99 -0
- package/dist/schemas/signature-profile.d.ts.map +1 -0
- package/dist/schemas/signature-profile.js +197 -0
- package/dist/schemas/signature-profile.js.map +1 -0
- package/dist/schemas/test-map.d.ts +4 -0
- package/dist/schemas/test-map.d.ts.map +1 -1
- package/dist/schemas/test-map.js +12 -0
- package/dist/schemas/test-map.js.map +1 -1
- package/dist/schemas/verdict.d.ts +19 -4
- package/dist/schemas/verdict.d.ts.map +1 -1
- package/dist/schemas/verdict.js +42 -5
- package/dist/schemas/verdict.js.map +1 -1
- package/dist/strictness.d.ts +102 -0
- package/dist/strictness.d.ts.map +1 -0
- package/dist/strictness.js +92 -0
- package/dist/strictness.js.map +1 -0
- package/dist/supervision/index.js +6 -6
- package/dist/supervision/index.js.map +1 -1
- package/dist/twin-paths.d.ts +125 -0
- package/dist/twin-paths.d.ts.map +1 -0
- package/dist/twin-paths.js +233 -0
- package/dist/twin-paths.js.map +1 -0
- package/dist/verdict/behavior.d.ts.map +1 -1
- package/dist/verdict/behavior.js +284 -10
- package/dist/verdict/behavior.js.map +1 -1
- package/dist/verdict/cause.d.ts.map +1 -1
- package/dist/verdict/cause.js +18 -0
- package/dist/verdict/cause.js.map +1 -1
- package/dist/verdict/evaluate.d.ts +62 -0
- package/dist/verdict/evaluate.d.ts.map +1 -1
- package/dist/verdict/evaluate.js +300 -13
- package/dist/verdict/evaluate.js.map +1 -1
- package/dist/verdict/index.d.ts +3 -3
- package/dist/verdict/index.d.ts.map +1 -1
- package/dist/verdict/index.js +3 -3
- package/dist/verdict/index.js.map +1 -1
- package/dist/verdict/pack-verifiers.d.ts +17 -3
- package/dist/verdict/pack-verifiers.d.ts.map +1 -1
- package/dist/verdict/pack-verifiers.js +47 -8
- package/dist/verdict/pack-verifiers.js.map +1 -1
- package/dist/verdict/registry.d.ts +19 -0
- package/dist/verdict/registry.d.ts.map +1 -1
- package/dist/verdict/registry.js +21 -0
- package/dist/verdict/registry.js.map +1 -1
- package/package.json +1 -1
package/dist/verdict/evaluate.js
CHANGED
|
@@ -395,7 +395,15 @@ function exactValueEchoFailure(operation, actionRecord, persistenceRecord) {
|
|
|
395
395
|
`'${labelOf(persistenceRecord)}' observed no persisted fields to echo the ` +
|
|
396
396
|
`'${operation}' input against`);
|
|
397
397
|
}
|
|
398
|
+
// Declared volatile fields (E18a): the adapter states that the server
|
|
399
|
+
// computes these keys itself, so the entered value is not expected to
|
|
400
|
+
// survive. That is a DECLARED fact about the app, never a silently
|
|
401
|
+
// ignored mismatch: every skip is reported (volatileEchoSkips) and
|
|
402
|
+
// surfaces as a report advisory.
|
|
403
|
+
const volatile = volatileFieldsOf(persistenceRecord);
|
|
398
404
|
for (const key of Object.keys(entered).sort()) {
|
|
405
|
+
if (volatile.includes(key))
|
|
406
|
+
continue;
|
|
399
407
|
const enteredValue = entered[key];
|
|
400
408
|
if (!isJsonValue(enteredValue))
|
|
401
409
|
continue;
|
|
@@ -408,6 +416,99 @@ function exactValueEchoFailure(operation, actionRecord, persistenceRecord) {
|
|
|
408
416
|
}
|
|
409
417
|
return null;
|
|
410
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* The volatile field keys a persistence record's adapter DECLARED
|
|
421
|
+
* (adapter `volatileFields`, stamped into the record payload by the
|
|
422
|
+
* witness). An adapter that declares none behaves exactly as before:
|
|
423
|
+
* the exact-value echo applies to every entered key.
|
|
424
|
+
*
|
|
425
|
+
* Args:
|
|
426
|
+
* record: a witnessed persistence record.
|
|
427
|
+
*
|
|
428
|
+
* Returns:
|
|
429
|
+
* string[]: the declared keys, sorted and deduplicated.
|
|
430
|
+
*/
|
|
431
|
+
export function volatileFieldsOf(record) {
|
|
432
|
+
const entry = asRecord(record);
|
|
433
|
+
if (entry === null)
|
|
434
|
+
return [];
|
|
435
|
+
const declared = payloadOf(entry)?.['volatileFields'];
|
|
436
|
+
if (!Array.isArray(declared))
|
|
437
|
+
return [];
|
|
438
|
+
const seen = {};
|
|
439
|
+
for (const key of declared) {
|
|
440
|
+
if (typeof key === 'string' && key.length > 0)
|
|
441
|
+
seen[key] = true;
|
|
442
|
+
}
|
|
443
|
+
return Object.keys(seen).sort(compareStrings);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The entered keys of the echo check, whichever channel carried them.
|
|
447
|
+
* Both are engine-issued sealed record payloads, never suite-authored:
|
|
448
|
+
* - browser: the `ui.action` anchor's DECLARED INPUT `fields`;
|
|
449
|
+
* - observe: there is no anchor, so the witness-observed request
|
|
450
|
+
* `observedFields` ride the persistence record itself (the same
|
|
451
|
+
* source observedEchoFailure grades against).
|
|
452
|
+
*
|
|
453
|
+
* An anchor's `fields` always wins when it is a plain object, empty or
|
|
454
|
+
* not: an anchor that declares no input is not an invitation to read
|
|
455
|
+
* another source. Either way, a declared volatile key is only reported
|
|
456
|
+
* when the source actually carries a value for it.
|
|
457
|
+
*
|
|
458
|
+
* Args:
|
|
459
|
+
* anchorOrNull: the witnessed UI-action record, or null when the
|
|
460
|
+
* evidence came from the observe channel.
|
|
461
|
+
* persistence: the matched persistence record (observed values).
|
|
462
|
+
*
|
|
463
|
+
* Returns:
|
|
464
|
+
* Record<string, unknown> | null: the entered keys, or null when the
|
|
465
|
+
* record carries neither source.
|
|
466
|
+
*/
|
|
467
|
+
function enteredFieldsOf(anchorOrNull, persistence) {
|
|
468
|
+
const anchor = asRecord(anchorOrNull);
|
|
469
|
+
if (anchor !== null) {
|
|
470
|
+
const declared = payloadOf(anchor)?.['fields'];
|
|
471
|
+
if (isPlainObject(declared))
|
|
472
|
+
return declared;
|
|
473
|
+
}
|
|
474
|
+
const observed = payloadOf(persistence)?.['observedFields'];
|
|
475
|
+
return isPlainObject(observed) ? observed : null;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The entered keys the echo check SKIPPED because the adapter declared
|
|
479
|
+
* them volatile, with the values the engine actually observed. The
|
|
480
|
+
* caller turns this into a visible report note — a skip is a fact the
|
|
481
|
+
* owner must see, never a quietly dropped mismatch.
|
|
482
|
+
*
|
|
483
|
+
* A satisfied obligation is evidence on either channel: with a
|
|
484
|
+
* `ui.action` anchor the entered values are the anchor's declared
|
|
485
|
+
* input fields, and on the observe channel (suite-driven browser over
|
|
486
|
+
* the session proxy) they are the witness-observed request fields the
|
|
487
|
+
* persistence record itself carries. Only keys the journey actually
|
|
488
|
+
* entered are reported — a declared field nobody sent is never a skip.
|
|
489
|
+
*
|
|
490
|
+
* Args:
|
|
491
|
+
* actionRecord: the witnessed UI-action record, or null when the
|
|
492
|
+
* evidence came from the observe channel.
|
|
493
|
+
* persistenceRecord: the matched persistence record (observed values).
|
|
494
|
+
*
|
|
495
|
+
* Returns:
|
|
496
|
+
* Array<{field: string, entered: unknown, persisted: unknown}>: one
|
|
497
|
+
* entry per declared-volatile key the journey actually entered.
|
|
498
|
+
*/
|
|
499
|
+
export function volatileEchoSkips(actionRecord, persistenceRecord) {
|
|
500
|
+
const persistence = asRecord(persistenceRecord);
|
|
501
|
+
if (persistence === null)
|
|
502
|
+
return [];
|
|
503
|
+
const entered = enteredFieldsOf(actionRecord, persistence);
|
|
504
|
+
if (entered === null)
|
|
505
|
+
return [];
|
|
506
|
+
const persisted = payloadOf(persistence)?.['fields'];
|
|
507
|
+
const observed = isPlainObject(persisted) ? persisted : {};
|
|
508
|
+
return volatileFieldsOf(persistence)
|
|
509
|
+
.filter((field) => isJsonValue(entered[field]))
|
|
510
|
+
.map((field) => ({ field, entered: entered[field], persisted: observed[field] ?? null }));
|
|
511
|
+
}
|
|
411
512
|
/**
|
|
412
513
|
* Observe-channel echo (Phase 2): the witness-observed request fields
|
|
413
514
|
* a suite-driven test sent must be echoed exactly by the independently
|
|
@@ -435,7 +536,12 @@ function observedEchoFailure(operation, record) {
|
|
|
435
536
|
`'${labelOf(record)}' observed no persisted fields to echo the ` +
|
|
436
537
|
`'${operation}' request against`);
|
|
437
538
|
}
|
|
539
|
+
// Same declared-volatile rule as the UI-action echo (E18a): a key the
|
|
540
|
+
// adapter declares server-computed is not expected to echo back.
|
|
541
|
+
const volatile = volatileFieldsOf(record);
|
|
438
542
|
for (const key of Object.keys(observed).sort()) {
|
|
543
|
+
if (volatile.includes(key))
|
|
544
|
+
continue;
|
|
439
545
|
const observedValue = observed[key];
|
|
440
546
|
if (!isJsonValue(observedValue))
|
|
441
547
|
continue;
|
|
@@ -526,8 +632,12 @@ function persistencePostconditionFailure(obligation, operation, record, actionEn
|
|
|
526
632
|
}
|
|
527
633
|
const qualifying = delta.filter((key) => updateable.includes(key));
|
|
528
634
|
if (qualifying.length === 0) {
|
|
529
|
-
|
|
530
|
-
`[${[...delta].sort().join(', ')}]
|
|
635
|
+
if (delta.length > 0) {
|
|
636
|
+
return (`update postcondition violated: the UI action changed only [${[...delta].sort().join(', ')}], ` +
|
|
637
|
+
'which is not a user-editable field; the update test must change one of ' +
|
|
638
|
+
`[${[...updateable].sort().join(', ')}]`);
|
|
639
|
+
}
|
|
640
|
+
return ('update postcondition violated: the engine-observed delta touches no classification-declared ' +
|
|
531
641
|
`updateable field (updateableFields: [${[...updateable].sort().join(', ')}])`);
|
|
532
642
|
}
|
|
533
643
|
return null;
|
|
@@ -587,10 +697,46 @@ function evaluateClaimEvidence(claim, evidence, obligation, primaryKey, resource
|
|
|
587
697
|
}
|
|
588
698
|
return verifier({ claim, obligation, evidence, primaryKey, resource, httpRoutes });
|
|
589
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Adds an unreachable-UI hint only when a complete host route inventory
|
|
702
|
+
* contains no update-capable endpoint linked to the obligation resource.
|
|
703
|
+
*
|
|
704
|
+
* Args:
|
|
705
|
+
* outcome: the persistence grader result.
|
|
706
|
+
* operation: the obligation operation.
|
|
707
|
+
* resource: the host-derived graph resource, when available.
|
|
708
|
+
* httpRoutes: the complete host-derived endpoint inventory.
|
|
709
|
+
*
|
|
710
|
+
* Returns:
|
|
711
|
+
* ClaimOutcome: the unchanged result or one with the route hint appended.
|
|
712
|
+
*/
|
|
713
|
+
function addMissingUpdateRouteHint(outcome, operation, resource, httpRoutes) {
|
|
714
|
+
if (operation !== 'update' ||
|
|
715
|
+
resource === null ||
|
|
716
|
+
resource === undefined ||
|
|
717
|
+
httpRoutes === null ||
|
|
718
|
+
httpRoutes === undefined ||
|
|
719
|
+
!('reason' in outcome) ||
|
|
720
|
+
!outcome.reason.includes('the UI action changed only')) {
|
|
721
|
+
return outcome;
|
|
722
|
+
}
|
|
723
|
+
const resourceName = resource.attributes['resourceName'];
|
|
724
|
+
if (typeof resourceName !== 'string')
|
|
725
|
+
return outcome;
|
|
726
|
+
const hasUpdateRoute = httpRoutes.some((route) => route.linkedResourceName === resourceName &&
|
|
727
|
+
route.capabilities?.includes('crud-update') === true);
|
|
728
|
+
return hasUpdateRoute
|
|
729
|
+
? outcome
|
|
730
|
+
: {
|
|
731
|
+
...outcome,
|
|
732
|
+
reason: `${outcome.reason}; no observed UI request writes these fields — ` +
|
|
733
|
+
'the feature may be unreachable from the UI',
|
|
734
|
+
};
|
|
735
|
+
}
|
|
590
736
|
// Built-in registrations: persistence/crud semantics stay owned by this
|
|
591
737
|
// module; pack namespaces register through './pack-verifiers.js'.
|
|
592
738
|
registerContractVerifier('crud', (input) => crudClaimVerifier(input));
|
|
593
|
-
registerContractVerifier('persistence', (input) => persistenceClaimVerifier(input.claim, input.evidence, input.obligation, input.primaryKey));
|
|
739
|
+
registerContractVerifier('persistence', (input) => persistenceClaimVerifier(input.claim, input.evidence, input.obligation, input.primaryKey, input.resource, input.httpRoutes));
|
|
594
740
|
registerPackVerifiers();
|
|
595
741
|
/**
|
|
596
742
|
* Capability metadata for the persistence namespace (plan Phase 0 item 7,
|
|
@@ -697,7 +843,7 @@ registerContractCapabilities(CRUD_CAPABILITY);
|
|
|
697
843
|
* browser `missing` to `invalid` (the witness DID observe the state;
|
|
698
844
|
* the claim declared the operation and lied).
|
|
699
845
|
*/
|
|
700
|
-
function persistenceClaimVerifier(claim, evidence, obligation, primaryKey) {
|
|
846
|
+
function persistenceClaimVerifier(claim, evidence, obligation, primaryKey, resource, httpRoutes) {
|
|
701
847
|
const requiredOp = persistenceOperation(obligation.contract);
|
|
702
848
|
if (requiredOp === null) {
|
|
703
849
|
return {
|
|
@@ -815,13 +961,14 @@ function persistenceClaimVerifier(claim, evidence, obligation, primaryKey) {
|
|
|
815
961
|
// upgrades a bare browser missing to invalid (the witness DID observe
|
|
816
962
|
// state; the claim declared the operation and lied) — server first,
|
|
817
963
|
// then observe. Otherwise the browser outcome stands verbatim.
|
|
964
|
+
let outcome = browser;
|
|
818
965
|
if (browser.status === 'missing') {
|
|
819
966
|
const sharp = serverFailure ?? observeFailure;
|
|
820
967
|
if (sharp !== null) {
|
|
821
|
-
|
|
968
|
+
outcome = { status: 'invalid', reason: `${sharp} (obligation '${obligation.id}')` };
|
|
822
969
|
}
|
|
823
970
|
}
|
|
824
|
-
return
|
|
971
|
+
return addMissingUpdateRouteHint(outcome, requiredOp, resource, httpRoutes);
|
|
825
972
|
}
|
|
826
973
|
/**
|
|
827
974
|
* The BROWSER channel of the persistence grader — the historical rule
|
|
@@ -841,8 +988,9 @@ function browserAnchoredPersistenceClaim(claim, evidence, obligation, primaryKey
|
|
|
841
988
|
// ones do not. Satisfaction weight lives in requirement 2, the
|
|
842
989
|
// engine-observed persistence read.
|
|
843
990
|
const actions = evidence.filter((entry) => entry.record.kind === UI_ACTION_KIND);
|
|
844
|
-
const
|
|
991
|
+
const matchingActions = actions.filter((entry) => isProvenancedRecord(entry.record) &&
|
|
845
992
|
payloadOf(entry.record)?.['operation'] === requiredOp);
|
|
993
|
+
const matchingAction = matchingActions[0];
|
|
846
994
|
if (matchingAction === undefined) {
|
|
847
995
|
if (actions.length === 0) {
|
|
848
996
|
return {
|
|
@@ -898,10 +1046,74 @@ function browserAnchoredPersistenceClaim(claim, evidence, obligation, primaryKey
|
|
|
898
1046
|
entry.record.kind.startsWith(PERSISTENCE_KIND_PREFIX) &&
|
|
899
1047
|
payloadOf(entry.record)?.['channel'] !== SERVER_CHANNEL);
|
|
900
1048
|
const witnessedPersistence = persistence.filter((entry) => entry.trust === 'witnessed');
|
|
901
|
-
const
|
|
1049
|
+
const actionAnchorId = typeof actionPayload['anchorId'] === 'string' && actionPayload['anchorId'].length > 0
|
|
1050
|
+
? actionPayload['anchorId']
|
|
1051
|
+
: null;
|
|
1052
|
+
const sameEntityActions = matchingActions.filter((entry) => {
|
|
902
1053
|
const entity = normalizeEntityId(payloadOf(entry.record)?.['entityId'], primaryKey);
|
|
903
1054
|
return entity.ok && entity.key === actionEntity.key;
|
|
904
1055
|
});
|
|
1056
|
+
const verifiedAnchorRecords = [];
|
|
1057
|
+
if (sameEntityActions.length > 1) {
|
|
1058
|
+
const anchorIds = sameEntityActions.map((entry) => {
|
|
1059
|
+
const anchorId = payloadOf(entry.record)?.['anchorId'];
|
|
1060
|
+
return typeof anchorId === 'string' && anchorId.length > 0 ? anchorId : null;
|
|
1061
|
+
});
|
|
1062
|
+
if (anchorIds.some((anchorId) => anchorId === null) || new Set(anchorIds).size !== anchorIds.length) {
|
|
1063
|
+
return {
|
|
1064
|
+
status: 'invalid',
|
|
1065
|
+
reason: `multiple '${UI_ACTION_KIND}' anchors target entity ${actionEntity.key}, but ` +
|
|
1066
|
+
'per-anchor snapshots are unavailable; refusing timing-dependent persistence evidence',
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
for (const [index, action] of sameEntityActions.entries()) {
|
|
1070
|
+
const anchorId = anchorIds[index];
|
|
1071
|
+
const snapshot = witnessedPersistence.find((entry) => {
|
|
1072
|
+
const payload = payloadOf(entry.record);
|
|
1073
|
+
const entity = normalizeEntityId(payload?.['entityId'], primaryKey);
|
|
1074
|
+
return entity.ok && entity.key === actionEntity.key && payload?.['anchorId'] === anchorId;
|
|
1075
|
+
});
|
|
1076
|
+
if (snapshot === undefined) {
|
|
1077
|
+
return {
|
|
1078
|
+
status: 'missing',
|
|
1079
|
+
reason: `no per-anchor '${PERSISTENCE_KIND_PREFIX}*' snapshot for '${UI_ACTION_KIND}' ` +
|
|
1080
|
+
`anchor '${anchorId}' on entity ${actionEntity.key}`,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
const failure = persistencePostconditionFailure(obligation, requiredOp, snapshot.record, actionEntity.key);
|
|
1084
|
+
if (failure !== null) {
|
|
1085
|
+
return { status: 'invalid', reason: `${failure} (obligation '${obligation.id}')` };
|
|
1086
|
+
}
|
|
1087
|
+
if (requiredOp === 'create' || requiredOp === 'update') {
|
|
1088
|
+
const echoFailure = exactValueEchoFailure(requiredOp, action.record, snapshot.record);
|
|
1089
|
+
if (echoFailure !== null) {
|
|
1090
|
+
return { status: 'invalid', reason: `${echoFailure} (obligation '${obligation.id}')` };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const visible = evidence.find((entry) => {
|
|
1094
|
+
if (entry.record.kind !== UI_VISIBLE_KIND)
|
|
1095
|
+
return false;
|
|
1096
|
+
const payload = payloadOf(entry.record);
|
|
1097
|
+
const entity = normalizeEntityId(payload?.['entityId'], primaryKey);
|
|
1098
|
+
return entity.ok && entity.key === actionEntity.key && payload?.['anchorId'] === anchorId;
|
|
1099
|
+
});
|
|
1100
|
+
if (visible !== undefined &&
|
|
1101
|
+
fieldsDisagreement(payloadOf(visible.record)?.['fields'], payloadOf(snapshot.record)?.['fields']) !== null) {
|
|
1102
|
+
return {
|
|
1103
|
+
status: 'invalid',
|
|
1104
|
+
reason: `visible and persisted fields disagree (obligation '${obligation.id}')`,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
verifiedAnchorRecords.push(action.record, snapshot.record);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const sameEntity = witnessedPersistence.filter((entry) => {
|
|
1111
|
+
const payload = payloadOf(entry.record);
|
|
1112
|
+
const entity = normalizeEntityId(payload?.['entityId'], primaryKey);
|
|
1113
|
+
return (entity.ok &&
|
|
1114
|
+
entity.key === actionEntity.key &&
|
|
1115
|
+
(actionAnchorId === null || payload?.['anchorId'] === actionAnchorId));
|
|
1116
|
+
});
|
|
905
1117
|
if (sameEntity.length === 0) {
|
|
906
1118
|
const claimedPersistence = persistence.find((entry) => entry.trust === 'claimed');
|
|
907
1119
|
if (claimedPersistence !== undefined) {
|
|
@@ -969,8 +1181,13 @@ function browserAnchoredPersistenceClaim(claim, evidence, obligation, primaryKey
|
|
|
969
1181
|
const visible = evidence.find((entry) => {
|
|
970
1182
|
if (entry.record.kind !== UI_VISIBLE_KIND)
|
|
971
1183
|
return false;
|
|
972
|
-
const
|
|
973
|
-
|
|
1184
|
+
const payload = payloadOf(entry.record);
|
|
1185
|
+
const entity = normalizeEntityId(payload?.['entityId'], primaryKey);
|
|
1186
|
+
const visibleAnchorId = payload?.['anchorId'];
|
|
1187
|
+
const sameAnchor = actionAnchorId === null ||
|
|
1188
|
+
visibleAnchorId === actionAnchorId ||
|
|
1189
|
+
(sameEntityActions.length === 1 && visibleAnchorId === undefined);
|
|
1190
|
+
return entity.ok && entity.key === actionEntity.key && sameAnchor;
|
|
974
1191
|
});
|
|
975
1192
|
if (visible !== undefined) {
|
|
976
1193
|
const disagreement = fieldsDisagreement(payloadOf(visible.record)?.['fields'], payloadOf(matchingPersistence.record)?.['fields']);
|
|
@@ -983,8 +1200,9 @@ function browserAnchoredPersistenceClaim(claim, evidence, obligation, primaryKey
|
|
|
983
1200
|
}
|
|
984
1201
|
}
|
|
985
1202
|
const used = [
|
|
986
|
-
|
|
987
|
-
|
|
1203
|
+
...(verifiedAnchorRecords.length > 0
|
|
1204
|
+
? verifiedAnchorRecords
|
|
1205
|
+
: [matchingAction.record, matchingPersistence.record]),
|
|
988
1206
|
...(visible !== undefined ? [visible.record] : []),
|
|
989
1207
|
]
|
|
990
1208
|
.map((record) => (typeof record.recordId === 'string' ? record.recordId : ''));
|
|
@@ -1442,6 +1660,51 @@ function gradeCrudSession(params) {
|
|
|
1442
1660
|
.map((record) => (typeof record.recordId === 'string' ? record.recordId : ''));
|
|
1443
1661
|
return { status: 'satisfied', recordIds: sortedUnique(used) };
|
|
1444
1662
|
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Grades an Alembic obligation from engine-issued records only.
|
|
1665
|
+
*
|
|
1666
|
+
* Args:
|
|
1667
|
+
* obligation: the parsed obligation. Its contract starts with `alembic:`.
|
|
1668
|
+
* context: verdict context. Suite records are ignored.
|
|
1669
|
+
*
|
|
1670
|
+
* Returns:
|
|
1671
|
+
* VerdictOutcome: satisfied only when this process issued a passing witness.
|
|
1672
|
+
*/
|
|
1673
|
+
function gradeAlembicObligation(obligation, context) {
|
|
1674
|
+
const records = (context.engineAlembicRecords ?? []).filter((record) => record.obligationId === obligation.id && record.kind === 'alembic.witness');
|
|
1675
|
+
const passing = records.find((record) => {
|
|
1676
|
+
if (record.trust !== 'witnessed' || record.origin !== 'engine-observed')
|
|
1677
|
+
return false;
|
|
1678
|
+
const payload = record.payload;
|
|
1679
|
+
if (payload === null || typeof payload !== 'object')
|
|
1680
|
+
return false;
|
|
1681
|
+
const body = payload;
|
|
1682
|
+
return (body['producer'] === 'gateforge.engine' &&
|
|
1683
|
+
body['passed'] === true &&
|
|
1684
|
+
body['contract'] === obligation.contract &&
|
|
1685
|
+
typeof body['filesDigest'] === 'string' &&
|
|
1686
|
+
/^[0-9a-f]{64}$/.test(body['filesDigest']));
|
|
1687
|
+
});
|
|
1688
|
+
if (passing !== undefined) {
|
|
1689
|
+
return { verdict: 'satisfied', reason: null, recordIds: passing.recordId === undefined ? [] : [passing.recordId] };
|
|
1690
|
+
}
|
|
1691
|
+
const failed = records.find((record) => {
|
|
1692
|
+
const payload = record.payload;
|
|
1693
|
+
return payload !== null && typeof payload === 'object' && payload['passed'] === false;
|
|
1694
|
+
});
|
|
1695
|
+
const failedPayload = failed?.payload !== null && typeof failed?.payload === 'object'
|
|
1696
|
+
? failed.payload
|
|
1697
|
+
: null;
|
|
1698
|
+
const cause = typeof failedPayload?.['cause'] === 'string' ? failedPayload['cause'] : 'MIGRATION_ROUNDTRIP_FAILED';
|
|
1699
|
+
const detail = typeof failedPayload?.['detail'] === 'string'
|
|
1700
|
+
? failedPayload['detail']
|
|
1701
|
+
: 'no engine-executed Alembic witness exists for this obligation';
|
|
1702
|
+
return {
|
|
1703
|
+
verdict: 'missing',
|
|
1704
|
+
reason: `${cause}: ${detail}`,
|
|
1705
|
+
recordIds: failed?.recordId === undefined ? [] : [failed.recordId],
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1445
1708
|
/**
|
|
1446
1709
|
* Evaluates ONE obligation against the run's claims, records, waivers,
|
|
1447
1710
|
* classification, and injected clock (pin #9). Pure and deterministic:
|
|
@@ -1468,6 +1731,9 @@ export function evaluateObligation(obligation, context) {
|
|
|
1468
1731
|
.join('; ')}`);
|
|
1469
1732
|
}
|
|
1470
1733
|
const verified = parsedObligation.data;
|
|
1734
|
+
if (verified.contract.startsWith('alembic:')) {
|
|
1735
|
+
return gradeAlembicObligation(verified, context);
|
|
1736
|
+
}
|
|
1471
1737
|
const now = parseInstant(context.now);
|
|
1472
1738
|
// 1. Unclassified resources block (invariant 1); unresolved resources
|
|
1473
1739
|
// never reach this evaluator (the policy engine emits blocking
|
|
@@ -1649,6 +1915,19 @@ export function evaluateObligation(obligation, context) {
|
|
|
1649
1915
|
*/
|
|
1650
1916
|
export function evaluateObligations(obligations, context) {
|
|
1651
1917
|
parseInstant(context.now);
|
|
1918
|
+
// The declared test ids per obligation (the same claim parsing the
|
|
1919
|
+
// single-obligation path applies): report presentation needs to know
|
|
1920
|
+
// "mapped to <test>, not yet witnessed" — an inferred or prior-run
|
|
1921
|
+
// binding is a suggestion and never lands here.
|
|
1922
|
+
const declaredByObligation = new Map();
|
|
1923
|
+
for (const raw of Array.isArray(context.claims) ? context.claims : []) {
|
|
1924
|
+
const parsed = ClaimSchema.safeParse(raw);
|
|
1925
|
+
if (!parsed.success)
|
|
1926
|
+
continue;
|
|
1927
|
+
const ids = declaredByObligation.get(parsed.data.obligationId) ?? [];
|
|
1928
|
+
ids.push(parsed.data.testId);
|
|
1929
|
+
declaredByObligation.set(parsed.data.obligationId, ids);
|
|
1930
|
+
}
|
|
1652
1931
|
return obligations
|
|
1653
1932
|
.map((obligation) => {
|
|
1654
1933
|
const outcome = evaluateObligation(obligation, context);
|
|
@@ -1667,7 +1946,15 @@ export function evaluateObligations(obligations, context) {
|
|
|
1667
1946
|
verdict: outcome.verdict,
|
|
1668
1947
|
reason: outcome.reason,
|
|
1669
1948
|
});
|
|
1670
|
-
|
|
1949
|
+
const declaredTests = sortedUnique(declaredByObligation.get(obligation.id) ?? []);
|
|
1950
|
+
return {
|
|
1951
|
+
obligation,
|
|
1952
|
+
...outcome,
|
|
1953
|
+
trustTier,
|
|
1954
|
+
cause: mapped.cause,
|
|
1955
|
+
nextAction: mapped.nextAction,
|
|
1956
|
+
...(declaredTests.length > 0 ? { declaredTests } : {}),
|
|
1957
|
+
};
|
|
1671
1958
|
})
|
|
1672
1959
|
.sort((a, b) => compareStrings(a.obligation.id, b.obligation.id));
|
|
1673
1960
|
}
|