filegrc 0.12.1 → 0.12.3
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/model/v10.json +67 -1
- package/package.json +1 -1
- package/src/cli.js +16 -4
- package/src/files.js +14 -2
- package/src/git.js +398 -65
- package/src/index.js +1 -1
- package/src/obligations.js +1 -1
- package/src/reconciliation.js +799 -149
- package/src/server.js +10 -1
- package/src/state.js +9 -0
- package/src/validate.js +45 -7
- package/src/web.js +59 -2
- package/src/workflow-history-integrity.js +31 -0
package/model/v10.json
CHANGED
|
@@ -7485,6 +7485,71 @@
|
|
|
7485
7485
|
"coverage"
|
|
7486
7486
|
]
|
|
7487
7487
|
},
|
|
7488
|
+
"reconciliation-dismissal": {
|
|
7489
|
+
"title": "Reconciliation dismissal",
|
|
7490
|
+
"pluralTitle": "Reconciliation dismissals",
|
|
7491
|
+
"group": "repository",
|
|
7492
|
+
"collection": "reconciliation-dismissals",
|
|
7493
|
+
"description": "A reviewed decision that one exact Git transition candidate is a false positive and does not represent a real policy event.",
|
|
7494
|
+
"guidance": {
|
|
7495
|
+
"policyBasis": "Direct file changes can resemble real operational events. This record preserves the reviewer, date, rationale, and exact candidate fingerprint when management confirms that no event occurred.",
|
|
7496
|
+
"cadence": "Create only from a current reconciliation candidate. A later change produces a different fingerprint and requires its own review."
|
|
7497
|
+
},
|
|
7498
|
+
"required": [
|
|
7499
|
+
"transitionFingerprint",
|
|
7500
|
+
"eventType",
|
|
7501
|
+
"subjectResourceId",
|
|
7502
|
+
"reviewedByIds",
|
|
7503
|
+
"reviewedOn",
|
|
7504
|
+
"rationale"
|
|
7505
|
+
],
|
|
7506
|
+
"fields": {
|
|
7507
|
+
"transitionFingerprint": {
|
|
7508
|
+
"type": "string",
|
|
7509
|
+
"label": "Transition fingerprint"
|
|
7510
|
+
},
|
|
7511
|
+
"eventType": {
|
|
7512
|
+
"type": "string",
|
|
7513
|
+
"label": "Candidate event type",
|
|
7514
|
+
"filter": true
|
|
7515
|
+
},
|
|
7516
|
+
"subjectResourceId": {
|
|
7517
|
+
"type": "id",
|
|
7518
|
+
"relationGroup": "event-subject",
|
|
7519
|
+
"label": "Candidate subject"
|
|
7520
|
+
},
|
|
7521
|
+
"reviewedByIds": {
|
|
7522
|
+
"type": "array",
|
|
7523
|
+
"items": "id",
|
|
7524
|
+
"relation": [
|
|
7525
|
+
"person"
|
|
7526
|
+
],
|
|
7527
|
+
"label": "Reviewed by"
|
|
7528
|
+
},
|
|
7529
|
+
"reviewedOn": {
|
|
7530
|
+
"type": "date",
|
|
7531
|
+
"label": "Reviewed on"
|
|
7532
|
+
},
|
|
7533
|
+
"rationale": {
|
|
7534
|
+
"type": "string",
|
|
7535
|
+
"search": true
|
|
7536
|
+
}
|
|
7537
|
+
},
|
|
7538
|
+
"listFields": [
|
|
7539
|
+
"title",
|
|
7540
|
+
"eventType",
|
|
7541
|
+
"subjectResourceId",
|
|
7542
|
+
"reviewedOn"
|
|
7543
|
+
],
|
|
7544
|
+
"formFields": [
|
|
7545
|
+
"transitionFingerprint",
|
|
7546
|
+
"eventType",
|
|
7547
|
+
"subjectResourceId",
|
|
7548
|
+
"reviewedByIds",
|
|
7549
|
+
"reviewedOn",
|
|
7550
|
+
"rationale"
|
|
7551
|
+
]
|
|
7552
|
+
},
|
|
7488
7553
|
"collection-review": {
|
|
7489
7554
|
"title": "Collection review",
|
|
7490
7555
|
"pluralTitle": "Collection reviews",
|
|
@@ -9516,7 +9581,8 @@
|
|
|
9516
9581
|
"vendor",
|
|
9517
9582
|
"access-grant",
|
|
9518
9583
|
"vulnerability",
|
|
9519
|
-
"incident"
|
|
9584
|
+
"incident",
|
|
9585
|
+
"exception"
|
|
9520
9586
|
],
|
|
9521
9587
|
"evidence-source-record": [
|
|
9522
9588
|
"appointment",
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
import { planProgramAmendment } from "./program-amendment.js";
|
|
69
69
|
import { resolveProgram } from "./program.js";
|
|
70
70
|
import { resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
|
|
71
|
-
import { applyReconciliation, planReconciliation } from "./reconciliation.js";
|
|
71
|
+
import { applyReconciliation, dismissReconciliation, planReconciliation } from "./reconciliation.js";
|
|
72
72
|
import { markdownEntries } from "./resource-markdown.js";
|
|
73
73
|
import { effectiveResourceStatus } from "./resource-status.js";
|
|
74
74
|
import { searchResources } from "./search.js";
|
|
@@ -617,7 +617,17 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
617
617
|
return result;
|
|
618
618
|
}
|
|
619
619
|
if (command === "reconcile") {
|
|
620
|
-
|
|
620
|
+
if (flags.apply && flags.dismiss) throw new Error("Choose either --apply or --dismiss for one reconciliation candidate.");
|
|
621
|
+
const result = flags.dismiss
|
|
622
|
+
? await withWorkflowDelta(root, () => dismissReconciliation(root, {
|
|
623
|
+
candidateId: flags.candidate,
|
|
624
|
+
transitionFingerprint: flags.candidate,
|
|
625
|
+
reviewedById: flags.reviewer,
|
|
626
|
+
reviewedOn: flags["reviewed-on"],
|
|
627
|
+
rationale: flags.rationale,
|
|
628
|
+
confirmed: flags.yes === true
|
|
629
|
+
}))
|
|
630
|
+
: flags.apply
|
|
621
631
|
? await withWorkflowDelta(root, () => applyReconciliation(root, {
|
|
622
632
|
candidateId: flags.candidate,
|
|
623
633
|
transitionFingerprint: flags.candidate,
|
|
@@ -630,7 +640,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
630
640
|
}))
|
|
631
641
|
: await planReconciliation(root);
|
|
632
642
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
633
|
-
else if (flags.
|
|
643
|
+
else if (flags.dismiss) {
|
|
644
|
+
console.log(`Dismissed ${result.candidate.eventType}: recorded ${result.dismissal.id}.`);
|
|
645
|
+
} else if (flags.apply) {
|
|
634
646
|
console.log(`Reconciled ${result.candidate.eventType}: created ${result.event.id} and ${result.actions.length} linked tasks.`);
|
|
635
647
|
} else if (!result.candidates.length) {
|
|
636
648
|
console.log("No direct-file transitions need confirmation.");
|
|
@@ -1328,7 +1340,7 @@ Usage:
|
|
|
1328
1340
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
1329
1341
|
filegrc audit-readiness [audit-id] [--as-of YYYY-MM-DD] [--require-ready] [--json]
|
|
1330
1342
|
filegrc prepare-audit <audit-id> [--json]
|
|
1331
|
-
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes] [--program program-id] [--risk-level normal|high] [--json]
|
|
1343
|
+
filegrc reconcile [--preview|--apply --candidate fingerprint (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --yes|--dismiss --candidate fingerprint --reviewer person-id --reviewed-on YYYY-MM-DD --rationale text --yes] [--program program-id] [--risk-level normal|high] [--json]
|
|
1332
1344
|
filegrc external-reviewer-setup --scaffold
|
|
1333
1345
|
filegrc external-reviewer-setup <reviewer.json|-> [--preview|--yes] [--json]
|
|
1334
1346
|
filegrc next-audit-cycle <prior-audit-id> [cycle.json|-] --start YYYY-MM-DD --end YYYY-MM-DD [--preview|--yes] [--json]
|
package/src/files.js
CHANGED
|
@@ -22,7 +22,8 @@ export const INTERNAL_WORKFLOW_CAPABILITIES = Object.freeze({
|
|
|
22
22
|
collectionReviewReassessment: Symbol("collection-review-reassessment"),
|
|
23
23
|
reportingRouteSetProposal: Symbol("reporting-route-set-proposal"),
|
|
24
24
|
reportingRouteSetApproval: Symbol("reporting-route-set-approval"),
|
|
25
|
-
reportingRouteSetCancellation: Symbol("reporting-route-set-cancellation")
|
|
25
|
+
reportingRouteSetCancellation: Symbol("reporting-route-set-cancellation"),
|
|
26
|
+
reconciliationDismissal: Symbol("reconciliation-dismissal")
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
export async function createResource(input, record, options = {}) {
|
|
@@ -671,6 +672,9 @@ function assertImmutableWorkflowRecord(existing, next, options = {}, loaded = nu
|
|
|
671
672
|
const preservesExisting = (allowed = []) => [...new Set([...Object.keys(existing), ...Object.keys(next)])].every((key) => (
|
|
672
673
|
allowed.includes(key) || JSON.stringify(next[key]) === JSON.stringify(existing[key])
|
|
673
674
|
));
|
|
675
|
+
if (existing.type === "reconciliation-dismissal" && !preservesExisting()) {
|
|
676
|
+
throw new Error(`Reconciliation dismissal "${existing.id}" is immutable because it records a fingerprint-bound review decision.`);
|
|
677
|
+
}
|
|
674
678
|
if (existing.type === "collection-review" && ["active", "retired"].includes(existing.status)) {
|
|
675
679
|
const retirement = options.workflowCapability === INTERNAL_WORKFLOW_CAPABILITIES.collectionReviewReassessment
|
|
676
680
|
&& existing.status === "active"
|
|
@@ -908,6 +912,13 @@ function finalizedOccurrenceUsingProof(loaded, targetId) {
|
|
|
908
912
|
}
|
|
909
913
|
|
|
910
914
|
function assertSpecializedWorkflowCreate(record, options = {}, loaded = null) {
|
|
915
|
+
if (
|
|
916
|
+
record?.type === "reconciliation-dismissal"
|
|
917
|
+
&& options.workflowCapability !== INTERNAL_WORKFLOW_CAPABILITIES.reconciliationDismissal
|
|
918
|
+
&& options.lifecycleOperation !== "model-migration"
|
|
919
|
+
) {
|
|
920
|
+
throw new Error(`Reconciliation dismissal "${record.id || ""}" is workflow-managed. Dismiss the current transition candidate instead of creating it directly.`);
|
|
921
|
+
}
|
|
911
922
|
if (
|
|
912
923
|
record?.type === "collection-review"
|
|
913
924
|
&& options.workflowCapability !== INTERNAL_WORKFLOW_CAPABILITIES.collectionReviewReassessment
|
|
@@ -984,7 +995,8 @@ async function deleteResourceUnlocked(input, type, id, options) {
|
|
|
984
995
|
assertRevision(source, options.expectedRevision, "The record");
|
|
985
996
|
const record = JSON.parse(source);
|
|
986
997
|
if (
|
|
987
|
-
|
|
998
|
+
record.type === "reconciliation-dismissal"
|
|
999
|
+
|| (record.type === "obligation-occurrence" && ["reconciled", "superseded"].includes(record.status))
|
|
988
1000
|
|| (record.type === "audit-population" && ["reconciled", "not-applicable", "superseded"].includes(record.status))
|
|
989
1001
|
|| (record.type === "obligation-event" && ["complete", "canceled"].includes(record.status))
|
|
990
1002
|
|| (record.type === "action-item" && record.obligationId && ["done", "canceled"].includes(record.status))
|