@contentrain/types 1.13.1 → 1.14.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/index.d.mts CHANGED
@@ -1,3 +1,9 @@
1
+ //#region src/canonical.d.ts
2
+ /** Recursively sort object keys — respects optional fieldOrder for top-level keys */
3
+ declare function sortKeys(obj: unknown, fieldOrder?: readonly string[]): unknown;
4
+ /** Canonical JSON serialization — deterministic output for stable git diffs */
5
+ declare function canonicalStringify(data: unknown, fieldOrder?: readonly string[]): string;
6
+ //#endregion
1
7
  //#region src/migration.d.ts
2
8
  /**
3
9
  * Version stamped into every migration document (`version` field on each root).
@@ -740,6 +746,574 @@ interface MigrationHandoff {
740
746
  notes?: string[];
741
747
  }
742
748
  //#endregion
749
+ //#region src/execution.d.ts
750
+ /**
751
+ * Version stamped into every execution document (`version` field on each root).
752
+ * A reader that sees a higher version than it knows should refuse rather than
753
+ * guess — a misread plan is a plan executed with the wrong scope.
754
+ */
755
+ declare const EXECUTION_CONTRACT_VERSION = 1;
756
+ /**
757
+ * Who is acting. `kind` is load-bearing for approval: an agent may never
758
+ * approve its own work, so the evaluator has to be able to tell an agent
759
+ * from a person without consulting Studio's user table.
760
+ */
761
+ interface ActorRef {
762
+ kind: 'human' | 'agent' | 'system';
763
+ /** Stable identifier in the issuing system (Studio user id, agent name). */
764
+ id: string;
765
+ /** Display name at the time of acting; advisory, never matched against. */
766
+ name?: string;
767
+ /** Project role held at the time of acting (e.g. "owner", "admin", "editor"). */
768
+ role?: string;
769
+ }
770
+ /**
771
+ * What is at stake in an operation, in ascending severity.
772
+ *
773
+ * The order is the contract, not a convenience: `RISK_CLASSES` is a ladder and
774
+ * a policy written for `bulk_content` is expected to also cover everything
775
+ * above it. The seven rungs come from the approval model — read-only work is
776
+ * automatic, content edits follow project policy, and anything that leaves the
777
+ * repository (money, third parties, production) is a separate decision from
778
+ * whether the content is correct.
779
+ */
780
+ declare const RISK_CLASSES: readonly ["read_only", "low_risk_content", "bulk_content", "destructive_schema", "external_effect", "financially_material", "deployment"];
781
+ type RiskClass = (typeof RISK_CLASSES)[number];
782
+ /** Position on the ladder; higher is more severe. */
783
+ declare function riskRank(risk: RiskClass): number;
784
+ /**
785
+ * The most severe of the given classes — the class a multi-step plan is
786
+ * evaluated against. A plan is exactly as safe as its worst step, so a
787
+ * read-only survey that ends in a deploy is a deploy.
788
+ */
789
+ declare function highestRisk(risks: readonly RiskClass[]): RiskClass;
790
+ /**
791
+ * The three moments at which a decision is asked for. They are distinct
792
+ * questions and collapsing them is the mistake this type exists to prevent:
793
+ * approving *what will be done* is not approving *what was produced*, and
794
+ * neither is permission to put it in front of the public.
795
+ */
796
+ declare const APPROVAL_GATES: readonly ["plan", "change", "release"];
797
+ type ApprovalGate = (typeof APPROVAL_GATES)[number];
798
+ /**
799
+ * How many people, and which, must say yes. Deliberately three modes: a
800
+ * generic workflow canvas was considered and rejected — policy-based approval
801
+ * covers every case the product actually has.
802
+ */
803
+ declare const APPROVAL_MODES: readonly ["auto", "single", "quorum"];
804
+ type ApprovalMode = (typeof APPROVAL_MODES)[number];
805
+ /**
806
+ * One rule in `.contentrain/approval-policies.json`: for operations at or
807
+ * above `risk`, this gate is required in this mode.
808
+ *
809
+ * Rules are additive. Every matching rule contributes a requirement; nothing a
810
+ * rule says can remove a requirement another rule created. A policy file can
811
+ * therefore only ever make a project stricter, which is what makes it safe to
812
+ * merge one from a template.
813
+ */
814
+ interface ApprovalRule {
815
+ /** Applies to this class and — because the classes are a ladder — everything above it. */
816
+ risk: RiskClass;
817
+ gate: ApprovalGate;
818
+ mode: ApprovalMode;
819
+ /** Quorum only: how many distinct approvers are needed. Ignored otherwise. */
820
+ min_approvals?: number;
821
+ /** Roles that may satisfy this rule. Empty/absent = any authorised reviewer. */
822
+ roles?: string[];
823
+ /** Narrow the rule to certain models. Absent = every model. */
824
+ models?: string[];
825
+ /** Narrow the rule to certain locales. Absent = every locale. */
826
+ locales?: string[];
827
+ }
828
+ /**
829
+ * `.contentrain/approval-policies.json`. Lives in git, beside the content it
830
+ * governs, so that the policy in force is the policy on the branch — a policy
831
+ * stored only in a database can be changed after a plan is built and before it
832
+ * is approved.
833
+ */
834
+ interface ApprovalPolicyFile {
835
+ version: number;
836
+ /**
837
+ * Applied at the `change` gate when no rule matches at all. Absent means no
838
+ * rule matched and therefore nothing is required — the policy's silence is
839
+ * taken literally rather than filled in with each consumer's idea of a
840
+ * sensible default.
841
+ */
842
+ default_mode?: ApprovalMode;
843
+ rules: ApprovalRule[];
844
+ /**
845
+ * Whether an actor may approve a plan they themselves created. Defaults to
846
+ * false; an agent can never self-approve regardless of this setting.
847
+ */
848
+ allow_self_approval?: boolean;
849
+ }
850
+ /**
851
+ * An outstanding or satisfied demand for a decision, derived from a rule.
852
+ * `because` carries the rule's risk class so a UI can say *why* a gate
853
+ * appeared without re-running the policy.
854
+ */
855
+ interface ApprovalRequirement {
856
+ gate: ApprovalGate;
857
+ mode: ApprovalMode;
858
+ /** Distinct approvers still needed; 0 once satisfied. */
859
+ min_approvals: number;
860
+ roles?: string[];
861
+ /** The risk class of the rule that produced this requirement. */
862
+ because: RiskClass;
863
+ }
864
+ /**
865
+ * A decision that was actually given.
866
+ *
867
+ * A grant is bound to an exact `plan_hash`: if the plan changes, its grants no
868
+ * longer apply and the gate reopens. That binding is the whole point — an
869
+ * approval of "publish these 12 posts" must not carry over to a plan that
870
+ * publishes 400.
871
+ */
872
+ interface ApprovalGrant {
873
+ gate: ApprovalGate;
874
+ /** The plan this decision was given for. */
875
+ plan_hash: string;
876
+ /** The branch tip reviewed, for `change` grants on a produced diff. */
877
+ commit_sha?: string;
878
+ approver: ActorRef;
879
+ /** ISO 8601 UTC. */
880
+ approved_at: string;
881
+ /** ISO 8601 UTC; a grant past this instant does not count. */
882
+ expires_at?: string;
883
+ note?: string;
884
+ }
885
+ /**
886
+ * What an operation touches. Used twice with the same shape: on a plan it is
887
+ * the prediction, on a receipt it is what actually happened. Keeping one shape
888
+ * for both is deliberate — the diff between them is the most useful thing a
889
+ * receipt can show, and it is only computable if the two are comparable.
890
+ *
891
+ * Every field is optional and an absent field means "none", not "unknown": a
892
+ * plan that cannot enumerate what it will touch is not a plan that should be
893
+ * approved.
894
+ */
895
+ interface ExecutionScope {
896
+ models?: string[];
897
+ locales?: string[];
898
+ /** Entry ids, or `model:id` where the model is not implied by `models`. */
899
+ entries?: string[];
900
+ /** Routes/URLs affected — cutovers and redirect rewrites report these. */
901
+ routes?: string[];
902
+ /** Repository-relative file paths. */
903
+ files?: string[];
904
+ /** Asset ids or paths. */
905
+ assets?: string[];
906
+ /** Provider identifiers reached (deployment, email, CRM). */
907
+ providers?: string[];
908
+ /** Hosts contacted outside the project — the evidence behind `external_effect`. */
909
+ external_domains?: string[];
910
+ }
911
+ /** One tool invocation inside a plan. */
912
+ interface ExecutionStep {
913
+ id: string;
914
+ /** Tool name as the runner knows it (e.g. "contentrain_bulk"). */
915
+ tool: string;
916
+ /** One line, written for the person approving, not for the log. */
917
+ summary: string;
918
+ risk: RiskClass;
919
+ /** What this step alone touches; the plan's scope is the union. */
920
+ scope?: ExecutionScope;
921
+ /** Opaque tool arguments. Hashed with the rest of the plan. */
922
+ input?: Record<string, unknown>;
923
+ }
924
+ /** Predicted cost. Currency is ISO 4217; absent means the cost is not monetary. */
925
+ interface ExecutionEstimate {
926
+ currency?: string;
927
+ /** Money, in major units. */
928
+ cost?: number;
929
+ /** Model tokens, when the step runs an agent. */
930
+ tokens?: number;
931
+ duration_ms?: number;
932
+ /** Items the operation expects to write — entries, files, assets. */
933
+ items?: number;
934
+ }
935
+ /** Cost as measured. Same shape as the estimate so the two can be subtracted. */
936
+ type ExecutionCost = ExecutionEstimate;
937
+ /**
938
+ * How to undo the operation. A command, not a promise: if the runner cannot
939
+ * state the undo, `available` is false and the approver learns that *before*
940
+ * deciding rather than after failing.
941
+ */
942
+ interface RollbackPlan {
943
+ available: boolean;
944
+ /** Shell command or tool call that reverses the effect. */
945
+ command?: string;
946
+ /** Commit to reset the content branch to. */
947
+ revert_to?: string;
948
+ /** Why it cannot be undone, when `available` is false. */
949
+ reason?: string;
950
+ }
951
+ /** The repository state a plan was built against. */
952
+ interface ProjectRef {
953
+ branch?: string;
954
+ commit_sha?: string;
955
+ /** Content branch tip, when it differs from the base branch. */
956
+ content_commit_sha?: string;
957
+ }
958
+ /**
959
+ * An operation, fully described, before it runs.
960
+ *
961
+ * `plan_hash` is the identity of *what will happen*. It covers the steps,
962
+ * scope, risk, estimate and rollback, and excludes who built the plan and
963
+ * when — regenerating the same operation must produce the same hash, or
964
+ * idempotency and approval binding both break. Compute it with
965
+ * {@link computePlanHash}; never hand-roll it, because a hash computed two
966
+ * different ways is a gate that silently opens.
967
+ */
968
+ interface ExecutionPlan {
969
+ version: number;
970
+ id: string;
971
+ /** SHA-256 of the plan's semantic payload — see {@link computePlanHash}. */
972
+ plan_hash: string;
973
+ /** What this achieves, in the requester's words. */
974
+ intent: string;
975
+ /** The plan's class: the highest risk among its steps. */
976
+ risk: RiskClass;
977
+ steps: ExecutionStep[];
978
+ /** Union of the steps' scopes, stated once so an approver reads one list. */
979
+ scope: ExecutionScope;
980
+ estimate?: ExecutionEstimate;
981
+ rollback?: RollbackPlan;
982
+ /** Repository state the plan assumes; re-checked at execution time. */
983
+ project?: ProjectRef;
984
+ /** ISO 8601 UTC. Excluded from the hash. */
985
+ created_at?: string;
986
+ /** Excluded from the hash, but checked for self-approval. */
987
+ created_by?: ActorRef;
988
+ /** ISO 8601 UTC; a plan past this instant must be rebuilt, not run. */
989
+ expires_at?: string;
990
+ /**
991
+ * Caller-supplied key that makes a re-submission a no-op rather than a
992
+ * second run. Excluded from the hash: two runs of the same work differ by
993
+ * this key alone and must still share a plan hash.
994
+ */
995
+ idempotency_key?: string;
996
+ }
997
+ /**
998
+ * Run lifecycle. The main line is `draft → planned → awaiting_approval →
999
+ * approved → scheduled → queued → running → verifying → completed`; the rest
1000
+ * are the states a run can sit in when something interrupts it.
1001
+ */
1002
+ declare const RUN_STATUSES: readonly ["draft", "planned", "awaiting_approval", "approved", "scheduled", "queued", "running", "verifying", "completed", "paused", "needs_attention", "retrying", "failed", "cancelled", "expired", "rolled_back"];
1003
+ type RunStatus = (typeof RUN_STATUSES)[number];
1004
+ /** Statuses from which a run will not proceed on its own. */
1005
+ declare const TERMINAL_RUN_STATUSES: readonly ["completed", "failed", "cancelled", "expired", "rolled_back"];
1006
+ declare function isTerminalRunStatus(status: RunStatus): boolean;
1007
+ /**
1008
+ * A step boundary the runner survived. Checkpoints are what make a resumed
1009
+ * run skip work it already did instead of repeating it.
1010
+ */
1011
+ interface Checkpoint {
1012
+ step_id: string;
1013
+ status: 'completed' | 'skipped' | 'failed';
1014
+ /** ISO 8601 UTC. */
1015
+ at: string;
1016
+ /** What this step actually touched. */
1017
+ scope?: ExecutionScope;
1018
+ /** Commit written by this step, when it wrote one. */
1019
+ commit_sha?: string;
1020
+ message?: string;
1021
+ }
1022
+ /** One post-run assertion — a build, a link check, a parity score. */
1023
+ interface VerificationCheck {
1024
+ id: string;
1025
+ /** One line naming what was asserted. */
1026
+ name: string;
1027
+ passed: boolean;
1028
+ /** Measured value, when the check produced one. */
1029
+ value?: number | string;
1030
+ /** Threshold the value was compared against. */
1031
+ expected?: number | string;
1032
+ detail?: string;
1033
+ }
1034
+ /**
1035
+ * What happened, after the fact. The receipt is the durable record: it
1036
+ * survives the run, is exported for audit, and is the only artefact that can
1037
+ * answer "who allowed this and what did it actually change?".
1038
+ *
1039
+ * Release approvers are the entries of `approvals` whose `gate` is `release`
1040
+ * — there is deliberately no separate `release_approved_by` field, because two
1041
+ * places recording the same fact is two places that can disagree. Read them
1042
+ * with {@link approversFor}.
1043
+ */
1044
+ interface ExecutionReceipt {
1045
+ version: number;
1046
+ id: string;
1047
+ plan_id: string;
1048
+ /** The plan that ran. Must match the approvals' `plan_hash`. */
1049
+ plan_hash: string;
1050
+ status: RunStatus;
1051
+ actor: ActorRef;
1052
+ /** Every decision that permitted this run, plan/change/release alike. */
1053
+ approvals?: ApprovalGrant[];
1054
+ /** ISO 8601 UTC. */
1055
+ started_at: string;
1056
+ finished_at?: string;
1057
+ /** What was actually touched — compare against the plan's `scope`. */
1058
+ applied: ExecutionScope;
1059
+ checkpoints?: Checkpoint[];
1060
+ verification?: VerificationCheck[];
1061
+ /** Measured cost — compare against the plan's `estimate`. */
1062
+ cost?: ExecutionCost;
1063
+ /** The undo, as it stands now that the run has happened. */
1064
+ rollback?: RollbackPlan;
1065
+ /** Repository state the run left behind. */
1066
+ project?: ProjectRef;
1067
+ /** Set when `status` is `failed` or `needs_attention`. */
1068
+ error?: {
1069
+ code: string;
1070
+ message: string;
1071
+ step_id?: string;
1072
+ };
1073
+ }
1074
+ /** Approvers recorded on a receipt for one gate, in grant order. */
1075
+ declare function approversFor(receipt: ExecutionReceipt, gate: ApprovalGate): ActorRef[];
1076
+ /**
1077
+ * Where a build is published. Carries a *reference* to a secret, never a
1078
+ * secret: this document is written to git, and a token in git is a token
1079
+ * leaked. The reference is resolved at execution time by whoever holds the
1080
+ * vault.
1081
+ */
1082
+ interface DeploymentTarget {
1083
+ id: string;
1084
+ /** Provider identity (e.g. "netlify", "vercel", "cloudflare-pages"). */
1085
+ provider: string;
1086
+ /** Provider-side site/project identifier. */
1087
+ site_id?: string;
1088
+ branch?: string;
1089
+ /** Key naming the credential in the executing side's secret store. */
1090
+ secret_ref?: string;
1091
+ /** Non-secret provider settings. */
1092
+ config?: Record<string, string>;
1093
+ }
1094
+ declare const SCHEDULE_KINDS: readonly ["once", "recurring", "content_event", "git_event", "runtime_event", "external_webhook", "manual"];
1095
+ type ScheduleKind = (typeof SCHEDULE_KINDS)[number];
1096
+ /** What to do with an occurrence that was missed while the runner was down. */
1097
+ type MisfirePolicy = 'skip' | 'run_once' | 'run_all';
1098
+ interface AutomationSchedule {
1099
+ kind: ScheduleKind;
1100
+ /** ISO 8601 UTC, for `once`. */
1101
+ at?: string;
1102
+ /** Cron expression, for `recurring`. */
1103
+ cron?: string;
1104
+ /** IANA zone (e.g. "Europe/Istanbul"); cron without one is ambiguous twice a year. */
1105
+ timezone?: string;
1106
+ /** Event name, for the event kinds. */
1107
+ event?: string;
1108
+ misfire?: MisfirePolicy;
1109
+ }
1110
+ interface AutomationDefinition {
1111
+ version: number;
1112
+ id: string;
1113
+ name: string;
1114
+ enabled: boolean;
1115
+ schedule: AutomationSchedule;
1116
+ /** What the run should do, in the requester's words. */
1117
+ intent: string;
1118
+ /** Risk the automation declares; the policy is re-evaluated at fire time. */
1119
+ risk?: RiskClass;
1120
+ /** Budget ceiling per occurrence. */
1121
+ budget?: ExecutionEstimate;
1122
+ /** Deduplication key for the occurrence. */
1123
+ idempotency_key?: string;
1124
+ /** Bounded retry; absent means no retry. */
1125
+ max_retries?: number;
1126
+ }
1127
+ /** The origin state a delta is measured from. */
1128
+ interface SourceCursor {
1129
+ kind: 'rest_modified_after' | 'wxr_export' | 'bridge_inventory';
1130
+ /** ISO 8601 UTC, for `rest_modified_after`. */
1131
+ modified_after?: string;
1132
+ /** Export identifier, for `wxr_export`. */
1133
+ export_id?: string;
1134
+ /** Inventory hash, for `bridge_inventory` — the only kind that can prove deletions. */
1135
+ inventory_hash?: string;
1136
+ /** ISO 8601 UTC time the cursor was taken (T0). */
1137
+ taken_at: string;
1138
+ }
1139
+ declare const SOURCE_DELTA_OPS: readonly ["created", "updated", "deleted", "moved"];
1140
+ type SourceDeltaOp = (typeof SOURCE_DELTA_OPS)[number];
1141
+ /**
1142
+ * One record's delta.
1143
+ *
1144
+ * `conflict` is the case that cannot be resolved mechanically: the same record
1145
+ * changed at the origin *and* in the repository since the cursor. Taking
1146
+ * either side silently loses an edit, so the plan reports it and a human
1147
+ * decides.
1148
+ */
1149
+ interface SourceDeltaEntry {
1150
+ op: SourceDeltaOp;
1151
+ /** Origin identifier (WordPress post/term/attachment id). */
1152
+ wp_id: number;
1153
+ /** Origin object type (e.g. "post", "page", "attachment", "category"). */
1154
+ wp_type?: string;
1155
+ /** Where it lives in the store, when it is already mapped. */
1156
+ model?: string;
1157
+ entry_id?: string;
1158
+ locale?: string;
1159
+ /** Origin content fingerprint at cursor time and now. */
1160
+ fingerprint_before?: string;
1161
+ fingerprint_after?: string;
1162
+ /** For `moved`: the slugs, in that order. Redirects are generated from these. */
1163
+ slug_before?: string;
1164
+ slug_after?: string;
1165
+ /** True when the repository also changed this record since the cursor. */
1166
+ conflict?: boolean;
1167
+ /** Why this entry is here, for the person reading the plan. */
1168
+ detail?: string;
1169
+ }
1170
+ /**
1171
+ * A one-way transfer plan from the WordPress origin to the repository.
1172
+ *
1173
+ * Not a sync: there is no path back. The plan is produced, written, and only
1174
+ * then does `contentrain_reconcile` merge the result on the git side.
1175
+ */
1176
+ interface SourceDeltaPlan {
1177
+ version: number;
1178
+ /** ISO 8601 UTC. */
1179
+ generated_at: string;
1180
+ /** Origin state this delta is measured from. */
1181
+ cursor: SourceCursor;
1182
+ /** Origin state this delta brings the repository up to. */
1183
+ next_cursor?: SourceCursor;
1184
+ entries: SourceDeltaEntry[];
1185
+ /**
1186
+ * Whether deletions could be determined at all. A `rest_modified_after`
1187
+ * cursor cannot see them; a plan that says `false` must not be read as
1188
+ * "nothing was deleted".
1189
+ */
1190
+ deletions_detectable: boolean;
1191
+ /** Redirects implied by the `moved` entries. */
1192
+ redirects?: {
1193
+ from: string;
1194
+ to: string;
1195
+ status: 301 | 302 | 307 | 308;
1196
+ }[];
1197
+ /** Human-facing notes: truncation, skipped types, coverage limits. */
1198
+ warnings?: string[];
1199
+ }
1200
+ /**
1201
+ * The exact bytes `plan_hash` is computed over. Exposed because a hash is only
1202
+ * useful if a disagreement can be investigated: when two sides compute
1203
+ * different hashes for the same plan, comparing payloads shows which field
1204
+ * differs, and comparing hashes shows nothing.
1205
+ *
1206
+ * Excluded: `plan_hash` itself (it cannot cover itself), and `id`,
1207
+ * `created_at`, `created_by` and `idempotency_key` — who built a plan, when,
1208
+ * under which run id, and with which deduplication key do not change what the
1209
+ * plan will do. Everything else is covered, so a changed step, a widened
1210
+ * scope, a raised estimate or a withdrawn rollback all invalidate every
1211
+ * approval the plan had collected.
1212
+ */
1213
+ declare function planHashPayload(plan: ExecutionPlan): string;
1214
+ /**
1215
+ * SHA-256 of {@link planHashPayload}, lowercase hex.
1216
+ *
1217
+ * Async because it uses Web Crypto (`globalThis.crypto.subtle`), which is
1218
+ * available in Node 18+, Deno, Bun, workers and browsers alike — this package
1219
+ * is consumed in all of them and must not reach for `node:crypto`. A non-
1220
+ * cryptographic hash was considered and rejected: approvals are pinned to this
1221
+ * value, so a collision is an approval bypass, not a cache miss.
1222
+ */
1223
+ declare function computePlanHash(plan: ExecutionPlan): Promise<string>;
1224
+ //#endregion
1225
+ //#region src/approval.d.ts
1226
+ /**
1227
+ * Used when a project has no `.contentrain/approval-policies.json`.
1228
+ *
1229
+ * Read-only work proceeds; everything else wants one reviewer on the produced
1230
+ * diff. Deliberately stated as a value rather than buried in the evaluator so
1231
+ * a project can inspect it, extend it, or reject it — and so "what happens
1232
+ * without a policy file" has one answer instead of one per caller.
1233
+ *
1234
+ * Note this is a *default*, not the only gate in the system: whether a project
1235
+ * consults the evaluator at all is still governed by its `workflow` setting.
1236
+ */
1237
+ declare const DEFAULT_APPROVAL_POLICY: ApprovalPolicyFile;
1238
+ /**
1239
+ * Why a grant did not count. Machine-readable because the useful question is
1240
+ * never "is it blocked" but "I approved this, why is it still blocked".
1241
+ */
1242
+ type GrantRejection = /** Given for a different plan — the plan changed after the decision. */'plan_hash_mismatch' /** A `change` grant reviewed a different branch tip than the one presented. */ | 'commit_mismatch' /** Past its `expires_at`. */ | 'expired' /** An agent cannot approve. */ | 'agent_approver' /** The approver authored the plan and the policy forbids self-approval. */ | 'self_approval' /** The approver's role is not one the rule accepts. */ | 'role_not_permitted' /** This approver already counted toward the requirement. */ | 'duplicate_approver' /** No requirement at this gate — nothing for the grant to satisfy. */ | 'no_matching_requirement';
1243
+ interface RejectedGrant {
1244
+ grant: ApprovalGrant;
1245
+ reason: GrantRejection;
1246
+ }
1247
+ /** A requirement plus the decisions that have accumulated against it. */
1248
+ interface ApprovalStatus extends ApprovalRequirement {
1249
+ /** Distinct approvers counted so far, in grant order. */
1250
+ approvers: ActorRef[];
1251
+ /** Still needed; 0 means satisfied. */
1252
+ remaining: number;
1253
+ }
1254
+ interface ApprovalDecision {
1255
+ /** Every requirement is satisfied and the plan has not expired. */
1256
+ allowed: boolean;
1257
+ /** The class the plan was evaluated at — its own, or its worst step's. */
1258
+ risk: RiskClass;
1259
+ /** Every requirement the policy produced, satisfied or not. */
1260
+ requirements: ApprovalStatus[];
1261
+ /** The subset still needing approvers, in gate order. */
1262
+ outstanding: ApprovalStatus[];
1263
+ /** Decisions that were supplied but did not count, each with a reason. */
1264
+ rejected_grants: RejectedGrant[];
1265
+ /** One line per reason the plan cannot proceed; empty when it can. */
1266
+ reasons: string[];
1267
+ }
1268
+ interface ApprovalEvaluation {
1269
+ plan: ExecutionPlan;
1270
+ /** Absent falls back to {@link DEFAULT_APPROVAL_POLICY}. */
1271
+ policy?: ApprovalPolicyFile;
1272
+ /** Decisions collected so far. */
1273
+ grants?: readonly ApprovalGrant[];
1274
+ /**
1275
+ * The branch tip being presented for a `change` decision. When given, a
1276
+ * `change` grant must name this commit: a review of an older tip is not a
1277
+ * review of what is about to merge. When absent, `change` grants are matched
1278
+ * on `plan_hash` alone.
1279
+ */
1280
+ commit_sha?: string;
1281
+ /** ISO 8601 UTC. Passed in, never read from the clock. */
1282
+ now?: string;
1283
+ }
1284
+ /**
1285
+ * The class a plan is judged at: its declared risk, or its worst step's if
1286
+ * that is higher. A plan cannot understate itself — declaring `read_only`
1287
+ * while carrying a deploy step would otherwise walk straight past the policy.
1288
+ */
1289
+ declare function effectiveRisk(plan: ExecutionPlan): RiskClass;
1290
+ /**
1291
+ * The requirements a policy produces for a plan, before any decision is
1292
+ * collected. This is what a plan card renders: "this needs one reviewer on the
1293
+ * diff and two on release, because it deploys".
1294
+ *
1295
+ * Each matching rule contributes its own requirement and all of them must be
1296
+ * met. Merging two rules into one would need a rule for combining their modes,
1297
+ * roles and counts — and every such rule has a case where the result is
1298
+ * *looser* than one of its inputs. Keeping them separate is what makes the
1299
+ * policy additive in fact and not just in intent.
1300
+ *
1301
+ * `auto` rules are dropped rather than kept as zero-approver requirements: a
1302
+ * requirement nobody has to satisfy is noise on the card, and a stricter rule
1303
+ * matching the same gate still stands on its own.
1304
+ */
1305
+ declare function requiredApprovals(plan: ExecutionPlan, policy?: ApprovalPolicyFile): ApprovalRequirement[];
1306
+ /**
1307
+ * May this plan proceed?
1308
+ *
1309
+ * Answers with the full picture rather than a boolean: which requirements
1310
+ * exist, which are met, who met them, and — for every decision that was
1311
+ * supplied but did not count — why it did not. The last part is what makes
1312
+ * "I approved this, why is it still blocked?" answerable without reading the
1313
+ * evaluator's source.
1314
+ */
1315
+ declare function evaluateApproval(input: ApprovalEvaluation): ApprovalDecision;
1316
+ //#endregion
743
1317
  //#region src/provider.d.ts
744
1318
  /**
745
1319
  * A single file change within a plan.
@@ -1421,7 +1995,11 @@ declare const PATH_PATTERNS: {
1421
1995
  readonly config: ".contentrain/config.json";
1422
1996
  readonly context: ".contentrain/context.json";
1423
1997
  readonly vocabulary: ".contentrain/vocabulary.json";
1424
- readonly model: ".contentrain/models/{modelId}.json";
1998
+ readonly model: ".contentrain/models/{modelId}.json"; /** Detected source capabilities and the provider chosen for each — `CapabilityManifest`. */
1999
+ readonly capabilities: ".contentrain/capabilities.json"; /** Scheduled and event-driven agent work — `AutomationDefinition[]`. */
2000
+ readonly automations: ".contentrain/automations.json"; /** Which operations need whose approval — `ApprovalPolicyFile`. */
2001
+ readonly approvalPolicies: ".contentrain/approval-policies.json"; /** Source→destination URL map, the output of a migration's `moved` entries. */
2002
+ readonly redirects: ".contentrain/redirects.json";
1425
2003
  readonly content: {
1426
2004
  readonly singleton: ".contentrain/content/{domain}/{modelId}/{locale}.json";
1427
2005
  readonly collection: ".contentrain/content/{domain}/{modelId}/{locale}.json";
@@ -1437,6 +2015,30 @@ declare const PATH_PATTERNS: {
1437
2015
  readonly dictionary: ".contentrain/meta/{modelId}/{locale}.json";
1438
2016
  };
1439
2017
  };
2018
+ /**
2019
+ * Paths this repository reserves but does not yet write.
2020
+ *
2021
+ * Reserving them is the point. `.contentrain/` is a shared namespace: the
2022
+ * migration engine, Studio and a customer's own tooling all write into it. A
2023
+ * name claimed here cannot later be taken for something else, and — more
2024
+ * usefully — the tools that walk the directory know these four are *expected*
2025
+ * rather than stray.
2026
+ *
2027
+ * The contract for them is deliberately narrow and must stay that way until a
2028
+ * tool actually owns one:
2029
+ *
2030
+ * - `contentrain_doctor` and `contentrain_validate` ignore them. They are not
2031
+ * orphans, not broken content, and not the validator's business.
2032
+ * - `contentrain_reconcile` treats each as one opaque file: it takes the side
2033
+ * that changed, and reports `file_conflict` when both sides did. It never
2034
+ * merges their interiors, because it does not know their interiors.
2035
+ *
2036
+ * Both behaviours are pinned by tests in `@contentrain/mcp`, so a future
2037
+ * change to directory walking cannot quietly start flagging or auto-merging
2038
+ * these files.
2039
+ */
2040
+ declare const RESERVED_PATHS: readonly [".contentrain/capabilities.json", ".contentrain/automations.json", ".contentrain/approval-policies.json", ".contentrain/redirects.json"];
2041
+ declare function isReservedPath(path: string): boolean;
1440
2042
  /** Slug: lowercase alphanumeric with hyphens */
1441
2043
  declare const SLUG_PATTERN: RegExp;
1442
2044
  /** Entry ID: alphanumeric, 1-40 chars, starts with alphanumeric */
@@ -1734,10 +2336,6 @@ declare function validateAccept(value: string, accept: string): ValidationError[
1734
2336
  * or `maxSize` — byte length is not knowable from a stored path.
1735
2337
  */
1736
2338
  declare function validateFieldValue(value: unknown, fieldDef: FieldDef): ValidationError[];
1737
- /** Recursively sort object keys — respects optional fieldOrder for top-level keys */
1738
- declare function sortKeys(obj: unknown, fieldOrder?: readonly string[]): unknown;
1739
- /** Canonical JSON serialization — deterministic output for stable git diffs */
1740
- declare function canonicalStringify(data: unknown, fieldOrder?: readonly string[]): string;
1741
2339
  /** Generate a 12-character hex entry ID */
1742
2340
  declare function generateEntryId(): string;
1743
2341
  /**
@@ -1766,6 +2364,31 @@ declare function conflictId(conflict: {
1766
2364
  ours?: unknown;
1767
2365
  theirs?: unknown;
1768
2366
  }): string;
2367
+ /**
2368
+ * Read one frontmatter scalar: booleans, null, numbers, quoted strings with
2369
+ * their escapes decoded, or the text as written.
2370
+ *
2371
+ * Exported because it is a **contract**, not a utility. Two readers open these
2372
+ * documents — this package (through `parseMarkdownFrontmatter`, which
2373
+ * `@contentrain/mcp` re-exports) and `@contentrain/query`'s generator and Astro
2374
+ * loader. When each had its own copy they disagreed about what `"He said
2375
+ * \"Hi\""` says, which means the generated client and the content engine
2376
+ * disagreed about the same file. One implementation, imported by both.
2377
+ */
2378
+ declare function parseFrontmatterScalar(raw: string): unknown;
2379
+ /**
2380
+ * A scalar read as a string: quoting and escapes are resolved, but a value that
2381
+ * merely looks like a number or a boolean stays the text it was. Array items
2382
+ * use this, and so does any reader that knows from the model that a field is
2383
+ * string-typed — a SKU of `"007"` must not become the number 7.
2384
+ */
2385
+ declare function parseFrontmatterScalarString(raw: string): string;
2386
+ /**
2387
+ * Split an inline array's interior on commas that are not inside a quoted
2388
+ * item — `["a, b", c]` is two items, not three. Exported for the same reason
2389
+ * as {@link parseFrontmatterScalar}: both readers must split it the same way.
2390
+ */
2391
+ declare function splitFrontmatterList(inner: string): string[];
1769
2392
  /** Parse markdown frontmatter — extracts YAML frontmatter and body from a markdown string */
1770
2393
  declare function parseMarkdownFrontmatter(content: string): {
1771
2394
  frontmatter: Record<string, unknown>;
@@ -1774,5 +2397,5 @@ declare function parseMarkdownFrontmatter(content: string): {
1774
2397
  /** Serialize data + body into markdown frontmatter format */
1775
2398
  declare function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string;
1776
2399
  //#endregion
1777
- export { type ApplyPlanInput, AssetEntry, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DICTIONARY_TITLE_FIELD, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, RuntimeBinding, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, StackType, StringContext, SyncResult, TITLE_FIELD_TYPES, ValidationError, ValidationResult, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, canonicalStringify, componentSlot, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
2400
+ export { APPROVAL_GATES, APPROVAL_MODES, ActorRef, type ApplyPlanInput, ApprovalDecision, ApprovalEvaluation, ApprovalGate, ApprovalGrant, ApprovalMode, ApprovalPolicyFile, ApprovalRequirement, ApprovalRule, ApprovalStatus, AssetEntry, AutomationDefinition, AutomationSchedule, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, Checkpoint, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, DeploymentTarget, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, ExecutionCost, ExecutionEstimate, ExecutionPlan, ExecutionReceipt, ExecutionScope, ExecutionStep, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GrantRejection, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, MisfirePolicy, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, ProjectRef, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, RejectedGrant, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RiskClass, RollbackPlan, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, RunStatus, RuntimeBinding, SCHEDULE_KINDS, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, ScheduleKind, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, SourceCursor, SourceDeltaEntry, SourceDeltaOp, SourceDeltaPlan, StackType, StringContext, SyncResult, TERMINAL_RUN_STATUSES, TITLE_FIELD_TYPES, ValidationError, ValidationResult, VerificationCheck, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, approversFor, canonicalStringify, componentSlot, computePlanHash, conflictId, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isReservedPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
1778
2401
  //# sourceMappingURL=index.d.mts.map