@contentrain/types 1.13.1 → 1.15.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).
@@ -263,8 +269,30 @@ interface CapabilityManifest {
263
269
  site_url: string;
264
270
  access: {
265
271
  html_status?: number | null;
266
- rest_status?: number | null; /** Highest source-access rung actually achieved for this site. */
267
- achieved: SourceAccessKind;
272
+ rest_status?: number | null;
273
+ /**
274
+ * Highest source-access rung actually achieved for this site, or `null`
275
+ * when none was — the site answered nothing a rung describes.
276
+ *
277
+ * Nullable rather than optional, and rather than a `'none'` rung. Optional
278
+ * would let a producer omit the field and call that "not measured", which
279
+ * is the failure this field exists to prevent: a manifest that reports
280
+ * `rest_public` for every site makes the access ladder unreadable, and the
281
+ * ladder is what the coverage figures are computed from. Required-and-
282
+ * nullable makes "I reached nothing" a thing the producer has to say.
283
+ *
284
+ * A `'none'` rung was the other candidate and is worse: `SourceAccessKind`
285
+ * is also `RawProvenance.kind`, where it answers "how was this data
286
+ * obtained". A RawIR stamped `kind: 'none'` is a document that exists
287
+ * without having been obtained — an impossible state made representable in
288
+ * an unrelated contract. `SOURCE_ACCESS_LADDER` would stop being a ladder
289
+ * too: it is an ordered array and coverage arithmetic indexes into it.
290
+ *
291
+ * `null` reads with `rest_status: null` beside it as its evidence. Coverage
292
+ * arithmetic should drop these sites from the numerator, not score them
293
+ * zero — a site behind an access wall was not measured, it did not fail.
294
+ */
295
+ achieved: SourceAccessKind | null;
268
296
  };
269
297
  generator?: string | null;
270
298
  theme?: string | null;
@@ -740,6 +768,599 @@ interface MigrationHandoff {
740
768
  notes?: string[];
741
769
  }
742
770
  //#endregion
771
+ //#region src/execution.d.ts
772
+ /**
773
+ * Version stamped into every execution document (`version` field on each root).
774
+ * A reader that sees a higher version than it knows should refuse rather than
775
+ * guess — a misread plan is a plan executed with the wrong scope.
776
+ */
777
+ declare const EXECUTION_CONTRACT_VERSION = 1;
778
+ /**
779
+ * Who is acting. `kind` is load-bearing for approval: an agent may never
780
+ * approve its own work, so the evaluator has to be able to tell an agent
781
+ * from a person without consulting Studio's user table.
782
+ */
783
+ interface ActorRef {
784
+ kind: 'human' | 'agent' | 'system';
785
+ /** Stable identifier in the issuing system (Studio user id, agent name). */
786
+ id: string;
787
+ /** Display name at the time of acting; advisory, never matched against. */
788
+ name?: string;
789
+ /** Project role held at the time of acting (e.g. "owner", "admin", "editor"). */
790
+ role?: string;
791
+ }
792
+ /**
793
+ * What is at stake in an operation, in ascending severity.
794
+ *
795
+ * The order is the contract, not a convenience: `RISK_CLASSES` is a ladder and
796
+ * a policy written for `bulk_content` is expected to also cover everything
797
+ * above it. The seven rungs come from the approval model — read-only work is
798
+ * automatic, content edits follow project policy, and anything that leaves the
799
+ * repository (money, third parties, production) is a separate decision from
800
+ * whether the content is correct.
801
+ */
802
+ declare const RISK_CLASSES: readonly ["read_only", "low_risk_content", "bulk_content", "destructive_schema", "external_effect", "financially_material", "deployment"];
803
+ type RiskClass = (typeof RISK_CLASSES)[number];
804
+ /** Position on the ladder; higher is more severe. */
805
+ declare function riskRank(risk: RiskClass): number;
806
+ /**
807
+ * The most severe of the given classes — the class a multi-step plan is
808
+ * evaluated against. A plan is exactly as safe as its worst step, so a
809
+ * read-only survey that ends in a deploy is a deploy.
810
+ */
811
+ declare function highestRisk(risks: readonly RiskClass[]): RiskClass;
812
+ /**
813
+ * The three moments at which a decision is asked for. They are distinct
814
+ * questions and collapsing them is the mistake this type exists to prevent:
815
+ * approving *what will be done* is not approving *what was produced*, and
816
+ * neither is permission to put it in front of the public.
817
+ */
818
+ declare const APPROVAL_GATES: readonly ["plan", "change", "release"];
819
+ type ApprovalGate = (typeof APPROVAL_GATES)[number];
820
+ /**
821
+ * How many people, and which, must say yes. Deliberately three modes: a
822
+ * generic workflow canvas was considered and rejected — policy-based approval
823
+ * covers every case the product actually has.
824
+ */
825
+ declare const APPROVAL_MODES: readonly ["auto", "single", "quorum"];
826
+ type ApprovalMode = (typeof APPROVAL_MODES)[number];
827
+ /**
828
+ * One rule in `.contentrain/approval-policies.json`: for operations at or
829
+ * above `risk`, this gate is required in this mode.
830
+ *
831
+ * Rules are additive. Every matching rule contributes a requirement; nothing a
832
+ * rule says can remove a requirement another rule created. A policy file can
833
+ * therefore only ever make a project stricter, which is what makes it safe to
834
+ * merge one from a template.
835
+ */
836
+ interface ApprovalRule {
837
+ /** Applies to this class and — because the classes are a ladder — everything above it. */
838
+ risk: RiskClass;
839
+ gate: ApprovalGate;
840
+ mode: ApprovalMode;
841
+ /** Quorum only: how many distinct approvers are needed. Ignored otherwise. */
842
+ min_approvals?: number;
843
+ /** Roles that may satisfy this rule. Empty/absent = any authorised reviewer. */
844
+ roles?: string[];
845
+ /** Narrow the rule to certain models. Absent = every model. */
846
+ models?: string[];
847
+ /** Narrow the rule to certain locales. Absent = every locale. */
848
+ locales?: string[];
849
+ }
850
+ /**
851
+ * `.contentrain/approval-policies.json`. Lives in git, beside the content it
852
+ * governs, so that the policy in force is the policy on the branch — a policy
853
+ * stored only in a database can be changed after a plan is built and before it
854
+ * is approved.
855
+ */
856
+ interface ApprovalPolicyFile {
857
+ version: number;
858
+ /**
859
+ * Applied at the `change` gate when no rule matches at all. Absent means no
860
+ * rule matched and therefore nothing is required — the policy's silence is
861
+ * taken literally rather than filled in with each consumer's idea of a
862
+ * sensible default.
863
+ */
864
+ default_mode?: ApprovalMode;
865
+ rules: ApprovalRule[];
866
+ /**
867
+ * Whether an actor may approve a plan they themselves created. Defaults to
868
+ * false; an agent can never self-approve regardless of this setting.
869
+ */
870
+ allow_self_approval?: boolean;
871
+ }
872
+ /**
873
+ * An outstanding or satisfied demand for a decision, derived from a rule.
874
+ * `because` carries the rule's risk class so a UI can say *why* a gate
875
+ * appeared without re-running the policy.
876
+ */
877
+ interface ApprovalRequirement {
878
+ gate: ApprovalGate;
879
+ mode: ApprovalMode;
880
+ /** Distinct approvers still needed; 0 once satisfied. */
881
+ min_approvals: number;
882
+ roles?: string[];
883
+ /** The risk class of the rule that produced this requirement. */
884
+ because: RiskClass;
885
+ }
886
+ /**
887
+ * A decision that was actually given.
888
+ *
889
+ * A grant is bound to an exact `plan_hash`: if the plan changes, its grants no
890
+ * longer apply and the gate reopens. That binding is the whole point — an
891
+ * approval of "publish these 12 posts" must not carry over to a plan that
892
+ * publishes 400.
893
+ */
894
+ interface ApprovalGrant {
895
+ gate: ApprovalGate;
896
+ /** The plan this decision was given for. */
897
+ plan_hash: string;
898
+ /** The branch tip reviewed, for `change` grants on a produced diff. */
899
+ commit_sha?: string;
900
+ approver: ActorRef;
901
+ /** ISO 8601 UTC. */
902
+ approved_at: string;
903
+ /** ISO 8601 UTC; a grant past this instant does not count. */
904
+ expires_at?: string;
905
+ note?: string;
906
+ }
907
+ /**
908
+ * What an operation touches. Used twice with the same shape: on a plan it is
909
+ * the prediction, on a receipt it is what actually happened. Keeping one shape
910
+ * for both is deliberate — the diff between them is the most useful thing a
911
+ * receipt can show, and it is only computable if the two are comparable.
912
+ *
913
+ * Every field is optional and an absent field means "none", not "unknown": a
914
+ * plan that cannot enumerate what it will touch is not a plan that should be
915
+ * approved.
916
+ */
917
+ interface ExecutionScope {
918
+ models?: string[];
919
+ locales?: string[];
920
+ /** Entry ids, or `model:id` where the model is not implied by `models`. */
921
+ entries?: string[];
922
+ /** Routes/URLs affected — cutovers and redirect rewrites report these. */
923
+ routes?: string[];
924
+ /** Repository-relative file paths. */
925
+ files?: string[];
926
+ /** Asset ids or paths. */
927
+ assets?: string[];
928
+ /** Provider identifiers reached (deployment, email, CRM). */
929
+ providers?: string[];
930
+ /** Hosts contacted outside the project — the evidence behind `external_effect`. */
931
+ external_domains?: string[];
932
+ }
933
+ /** One tool invocation inside a plan. */
934
+ interface ExecutionStep {
935
+ id: string;
936
+ /** Tool name as the runner knows it (e.g. "contentrain_bulk"). */
937
+ tool: string;
938
+ /** One line, written for the person approving, not for the log. */
939
+ summary: string;
940
+ risk: RiskClass;
941
+ /** What this step alone touches; the plan's scope is the union. */
942
+ scope?: ExecutionScope;
943
+ /** Opaque tool arguments. Hashed with the rest of the plan. */
944
+ input?: Record<string, unknown>;
945
+ }
946
+ /**
947
+ * Predicted cost. Currency is ISO 4217; absent means the cost is not monetary.
948
+ *
949
+ * The non-monetary fields exist because cost is metered before it is priced:
950
+ * a run's bill is assembled from tokens, compute time, bytes stored and bytes
951
+ * transferred, and those are what a runner can actually measure at the moment
952
+ * it finishes a step. `cost` is the conversion, and it is often computed later
953
+ * from these.
954
+ */
955
+ interface ExecutionEstimate {
956
+ currency?: string;
957
+ /** Money, in major units. */
958
+ cost?: number;
959
+ /** Model tokens, when the step runs an agent. */
960
+ tokens?: number;
961
+ duration_ms?: number;
962
+ /**
963
+ * Items the operation expects to write — entries, files, assets. A count,
964
+ * not a size: use `bytes_stored` for how much was written.
965
+ */
966
+ items?: number;
967
+ /**
968
+ * Bytes the operation leaves at rest — a built artefact, an uploaded asset.
969
+ * Object storage bills for this per unit time, so it is the field a storage
970
+ * line on an invoice traces back to.
971
+ */
972
+ bytes_stored?: number;
973
+ /**
974
+ * Bytes the operation moves across the network — egress, and fetches made on
975
+ * the way. Billed separately from storage and by a different rate, which is
976
+ * why it is a separate field rather than one `bytes`: a run that stores 40 MB
977
+ * once and serves it a thousand times has one storage figure and a very
978
+ * different transfer figure.
979
+ */
980
+ bytes_out?: number;
981
+ }
982
+ /** Cost as measured. Same shape as the estimate so the two can be subtracted. */
983
+ type ExecutionCost = ExecutionEstimate;
984
+ /**
985
+ * How to undo the operation. A command, not a promise: if the runner cannot
986
+ * state the undo, `available` is false and the approver learns that *before*
987
+ * deciding rather than after failing.
988
+ */
989
+ interface RollbackPlan {
990
+ available: boolean;
991
+ /** Shell command or tool call that reverses the effect. */
992
+ command?: string;
993
+ /** Commit to reset the content branch to. */
994
+ revert_to?: string;
995
+ /** Why it cannot be undone, when `available` is false. */
996
+ reason?: string;
997
+ }
998
+ /** The repository state a plan was built against. */
999
+ interface ProjectRef {
1000
+ branch?: string;
1001
+ commit_sha?: string;
1002
+ /** Content branch tip, when it differs from the base branch. */
1003
+ content_commit_sha?: string;
1004
+ }
1005
+ /**
1006
+ * An operation, fully described, before it runs.
1007
+ *
1008
+ * `plan_hash` is the identity of *what will happen*. It covers the steps,
1009
+ * scope, risk, estimate and rollback, and excludes who built the plan and
1010
+ * when — regenerating the same operation must produce the same hash, or
1011
+ * idempotency and approval binding both break. Compute it with
1012
+ * {@link computePlanHash}; never hand-roll it, because a hash computed two
1013
+ * different ways is a gate that silently opens.
1014
+ */
1015
+ interface ExecutionPlan {
1016
+ version: number;
1017
+ id: string;
1018
+ /** SHA-256 of the plan's semantic payload — see {@link computePlanHash}. */
1019
+ plan_hash: string;
1020
+ /** What this achieves, in the requester's words. */
1021
+ intent: string;
1022
+ /** The plan's class: the highest risk among its steps. */
1023
+ risk: RiskClass;
1024
+ steps: ExecutionStep[];
1025
+ /** Union of the steps' scopes, stated once so an approver reads one list. */
1026
+ scope: ExecutionScope;
1027
+ estimate?: ExecutionEstimate;
1028
+ rollback?: RollbackPlan;
1029
+ /** Repository state the plan assumes; re-checked at execution time. */
1030
+ project?: ProjectRef;
1031
+ /** ISO 8601 UTC. Excluded from the hash. */
1032
+ created_at?: string;
1033
+ /** Excluded from the hash, but checked for self-approval. */
1034
+ created_by?: ActorRef;
1035
+ /** ISO 8601 UTC; a plan past this instant must be rebuilt, not run. */
1036
+ expires_at?: string;
1037
+ /**
1038
+ * Caller-supplied key that makes a re-submission a no-op rather than a
1039
+ * second run. Excluded from the hash: two runs of the same work differ by
1040
+ * this key alone and must still share a plan hash.
1041
+ */
1042
+ idempotency_key?: string;
1043
+ }
1044
+ /**
1045
+ * Run lifecycle. The main line is `draft → planned → awaiting_approval →
1046
+ * approved → scheduled → queued → running → verifying → completed`; the rest
1047
+ * are the states a run can sit in when something interrupts it.
1048
+ */
1049
+ declare const RUN_STATUSES: readonly ["draft", "planned", "awaiting_approval", "approved", "scheduled", "queued", "running", "verifying", "completed", "paused", "needs_attention", "retrying", "failed", "cancelled", "expired", "rolled_back"];
1050
+ type RunStatus = (typeof RUN_STATUSES)[number];
1051
+ /** Statuses from which a run will not proceed on its own. */
1052
+ declare const TERMINAL_RUN_STATUSES: readonly ["completed", "failed", "cancelled", "expired", "rolled_back"];
1053
+ declare function isTerminalRunStatus(status: RunStatus): boolean;
1054
+ /**
1055
+ * A step boundary the runner survived. Checkpoints are what make a resumed
1056
+ * run skip work it already did instead of repeating it.
1057
+ */
1058
+ interface Checkpoint {
1059
+ step_id: string;
1060
+ status: 'completed' | 'skipped' | 'failed';
1061
+ /** ISO 8601 UTC. */
1062
+ at: string;
1063
+ /** What this step actually touched. */
1064
+ scope?: ExecutionScope;
1065
+ /** Commit written by this step, when it wrote one. */
1066
+ commit_sha?: string;
1067
+ message?: string;
1068
+ }
1069
+ /** One post-run assertion — a build, a link check, a parity score. */
1070
+ interface VerificationCheck {
1071
+ id: string;
1072
+ /** One line naming what was asserted. */
1073
+ name: string;
1074
+ passed: boolean;
1075
+ /** Measured value, when the check produced one. */
1076
+ value?: number | string;
1077
+ /** Threshold the value was compared against. */
1078
+ expected?: number | string;
1079
+ detail?: string;
1080
+ }
1081
+ /**
1082
+ * What happened, after the fact. The receipt is the durable record: it
1083
+ * survives the run, is exported for audit, and is the only artefact that can
1084
+ * answer "who allowed this and what did it actually change?".
1085
+ *
1086
+ * Release approvers are the entries of `approvals` whose `gate` is `release`
1087
+ * — there is deliberately no separate `release_approved_by` field, because two
1088
+ * places recording the same fact is two places that can disagree. Read them
1089
+ * with {@link approversFor}.
1090
+ */
1091
+ interface ExecutionReceipt {
1092
+ version: number;
1093
+ id: string;
1094
+ plan_id: string;
1095
+ /** The plan that ran. Must match the approvals' `plan_hash`. */
1096
+ plan_hash: string;
1097
+ status: RunStatus;
1098
+ actor: ActorRef;
1099
+ /** Every decision that permitted this run, plan/change/release alike. */
1100
+ approvals?: ApprovalGrant[];
1101
+ /** ISO 8601 UTC. */
1102
+ started_at: string;
1103
+ finished_at?: string;
1104
+ /** What was actually touched — compare against the plan's `scope`. */
1105
+ applied: ExecutionScope;
1106
+ checkpoints?: Checkpoint[];
1107
+ verification?: VerificationCheck[];
1108
+ /** Measured cost — compare against the plan's `estimate`. */
1109
+ cost?: ExecutionCost;
1110
+ /** The undo, as it stands now that the run has happened. */
1111
+ rollback?: RollbackPlan;
1112
+ /** Repository state the run left behind. */
1113
+ project?: ProjectRef;
1114
+ /** Set when `status` is `failed` or `needs_attention`. */
1115
+ error?: {
1116
+ code: string;
1117
+ message: string;
1118
+ step_id?: string;
1119
+ };
1120
+ }
1121
+ /** Approvers recorded on a receipt for one gate, in grant order. */
1122
+ declare function approversFor(receipt: ExecutionReceipt, gate: ApprovalGate): ActorRef[];
1123
+ /**
1124
+ * Where a build is published. Carries a *reference* to a secret, never a
1125
+ * secret: this document is written to git, and a token in git is a token
1126
+ * leaked. The reference is resolved at execution time by whoever holds the
1127
+ * vault.
1128
+ */
1129
+ interface DeploymentTarget {
1130
+ id: string;
1131
+ /** Provider identity (e.g. "netlify", "vercel", "cloudflare-pages"). */
1132
+ provider: string;
1133
+ /** Provider-side site/project identifier. */
1134
+ site_id?: string;
1135
+ branch?: string;
1136
+ /** Key naming the credential in the executing side's secret store. */
1137
+ secret_ref?: string;
1138
+ /** Non-secret provider settings. */
1139
+ config?: Record<string, string>;
1140
+ }
1141
+ declare const SCHEDULE_KINDS: readonly ["once", "recurring", "content_event", "git_event", "runtime_event", "external_webhook", "manual"];
1142
+ type ScheduleKind = (typeof SCHEDULE_KINDS)[number];
1143
+ /** What to do with an occurrence that was missed while the runner was down. */
1144
+ type MisfirePolicy = 'skip' | 'run_once' | 'run_all';
1145
+ interface AutomationSchedule {
1146
+ kind: ScheduleKind;
1147
+ /** ISO 8601 UTC, for `once`. */
1148
+ at?: string;
1149
+ /** Cron expression, for `recurring`. */
1150
+ cron?: string;
1151
+ /** IANA zone (e.g. "Europe/Istanbul"); cron without one is ambiguous twice a year. */
1152
+ timezone?: string;
1153
+ /** Event name, for the event kinds. */
1154
+ event?: string;
1155
+ misfire?: MisfirePolicy;
1156
+ }
1157
+ interface AutomationDefinition {
1158
+ version: number;
1159
+ id: string;
1160
+ name: string;
1161
+ enabled: boolean;
1162
+ schedule: AutomationSchedule;
1163
+ /** What the run should do, in the requester's words. */
1164
+ intent: string;
1165
+ /** Risk the automation declares; the policy is re-evaluated at fire time. */
1166
+ risk?: RiskClass;
1167
+ /** Budget ceiling per occurrence. */
1168
+ budget?: ExecutionEstimate;
1169
+ /** Deduplication key for the occurrence. */
1170
+ idempotency_key?: string;
1171
+ /** Bounded retry; absent means no retry. */
1172
+ max_retries?: number;
1173
+ }
1174
+ /** The origin state a delta is measured from. */
1175
+ interface SourceCursor {
1176
+ kind: 'rest_modified_after' | 'wxr_export' | 'bridge_inventory';
1177
+ /** ISO 8601 UTC, for `rest_modified_after`. */
1178
+ modified_after?: string;
1179
+ /** Export identifier, for `wxr_export`. */
1180
+ export_id?: string;
1181
+ /** Inventory hash, for `bridge_inventory` — the only kind that can prove deletions. */
1182
+ inventory_hash?: string;
1183
+ /** ISO 8601 UTC time the cursor was taken (T0). */
1184
+ taken_at: string;
1185
+ }
1186
+ declare const SOURCE_DELTA_OPS: readonly ["created", "updated", "deleted", "moved"];
1187
+ type SourceDeltaOp = (typeof SOURCE_DELTA_OPS)[number];
1188
+ /**
1189
+ * One record's delta.
1190
+ *
1191
+ * `conflict` is the case that cannot be resolved mechanically: the same record
1192
+ * changed at the origin *and* in the repository since the cursor. Taking
1193
+ * either side silently loses an edit, so the plan reports it and a human
1194
+ * decides.
1195
+ */
1196
+ interface SourceDeltaEntry {
1197
+ op: SourceDeltaOp;
1198
+ /** Origin identifier (WordPress post/term/attachment id). */
1199
+ wp_id: number;
1200
+ /** Origin object type (e.g. "post", "page", "attachment", "category"). */
1201
+ wp_type?: string;
1202
+ /** Where it lives in the store, when it is already mapped. */
1203
+ model?: string;
1204
+ entry_id?: string;
1205
+ locale?: string;
1206
+ /** Origin content fingerprint at cursor time and now. */
1207
+ fingerprint_before?: string;
1208
+ fingerprint_after?: string;
1209
+ /** For `moved`: the slugs, in that order. Redirects are generated from these. */
1210
+ slug_before?: string;
1211
+ slug_after?: string;
1212
+ /** True when the repository also changed this record since the cursor. */
1213
+ conflict?: boolean;
1214
+ /** Why this entry is here, for the person reading the plan. */
1215
+ detail?: string;
1216
+ }
1217
+ /**
1218
+ * A one-way transfer plan from the WordPress origin to the repository.
1219
+ *
1220
+ * Not a sync: there is no path back. The plan is produced, written, and only
1221
+ * then does `contentrain_reconcile` merge the result on the git side.
1222
+ */
1223
+ interface SourceDeltaPlan {
1224
+ version: number;
1225
+ /** ISO 8601 UTC. */
1226
+ generated_at: string;
1227
+ /** Origin state this delta is measured from. */
1228
+ cursor: SourceCursor;
1229
+ /** Origin state this delta brings the repository up to. */
1230
+ next_cursor?: SourceCursor;
1231
+ entries: SourceDeltaEntry[];
1232
+ /**
1233
+ * Whether deletions could be determined at all. A `rest_modified_after`
1234
+ * cursor cannot see them; a plan that says `false` must not be read as
1235
+ * "nothing was deleted".
1236
+ */
1237
+ deletions_detectable: boolean;
1238
+ /** Redirects implied by the `moved` entries. */
1239
+ redirects?: {
1240
+ from: string;
1241
+ to: string;
1242
+ status: 301 | 302 | 307 | 308;
1243
+ }[];
1244
+ /** Human-facing notes: truncation, skipped types, coverage limits. */
1245
+ warnings?: string[];
1246
+ }
1247
+ /**
1248
+ * The exact bytes `plan_hash` is computed over. Exposed because a hash is only
1249
+ * useful if a disagreement can be investigated: when two sides compute
1250
+ * different hashes for the same plan, comparing payloads shows which field
1251
+ * differs, and comparing hashes shows nothing.
1252
+ *
1253
+ * Excluded: `plan_hash` itself (it cannot cover itself), and `id`,
1254
+ * `created_at`, `created_by` and `idempotency_key` — who built a plan, when,
1255
+ * under which run id, and with which deduplication key do not change what the
1256
+ * plan will do. Everything else is covered, so a changed step, a widened
1257
+ * scope, a raised estimate or a withdrawn rollback all invalidate every
1258
+ * approval the plan had collected.
1259
+ */
1260
+ declare function planHashPayload(plan: ExecutionPlan): string;
1261
+ /**
1262
+ * SHA-256 of {@link planHashPayload}, lowercase hex.
1263
+ *
1264
+ * Async because it uses Web Crypto (`globalThis.crypto.subtle`), which is
1265
+ * available in Node 18+, Deno, Bun, workers and browsers alike — this package
1266
+ * is consumed in all of them and must not reach for `node:crypto`. A non-
1267
+ * cryptographic hash was considered and rejected: approvals are pinned to this
1268
+ * value, so a collision is an approval bypass, not a cache miss.
1269
+ */
1270
+ declare function computePlanHash(plan: ExecutionPlan): Promise<string>;
1271
+ //#endregion
1272
+ //#region src/approval.d.ts
1273
+ /**
1274
+ * Used when a project has no `.contentrain/approval-policies.json`.
1275
+ *
1276
+ * Read-only work proceeds; everything else wants one reviewer on the produced
1277
+ * diff. Deliberately stated as a value rather than buried in the evaluator so
1278
+ * a project can inspect it, extend it, or reject it — and so "what happens
1279
+ * without a policy file" has one answer instead of one per caller.
1280
+ *
1281
+ * Note this is a *default*, not the only gate in the system: whether a project
1282
+ * consults the evaluator at all is still governed by its `workflow` setting.
1283
+ */
1284
+ declare const DEFAULT_APPROVAL_POLICY: ApprovalPolicyFile;
1285
+ /**
1286
+ * Why a grant did not count. Machine-readable because the useful question is
1287
+ * never "is it blocked" but "I approved this, why is it still blocked".
1288
+ */
1289
+ 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';
1290
+ interface RejectedGrant {
1291
+ grant: ApprovalGrant;
1292
+ reason: GrantRejection;
1293
+ }
1294
+ /** A requirement plus the decisions that have accumulated against it. */
1295
+ interface ApprovalStatus extends ApprovalRequirement {
1296
+ /** Distinct approvers counted so far, in grant order. */
1297
+ approvers: ActorRef[];
1298
+ /** Still needed; 0 means satisfied. */
1299
+ remaining: number;
1300
+ }
1301
+ interface ApprovalDecision {
1302
+ /** Every requirement is satisfied and the plan has not expired. */
1303
+ allowed: boolean;
1304
+ /** The class the plan was evaluated at — its own, or its worst step's. */
1305
+ risk: RiskClass;
1306
+ /** Every requirement the policy produced, satisfied or not. */
1307
+ requirements: ApprovalStatus[];
1308
+ /** The subset still needing approvers, in gate order. */
1309
+ outstanding: ApprovalStatus[];
1310
+ /** Decisions that were supplied but did not count, each with a reason. */
1311
+ rejected_grants: RejectedGrant[];
1312
+ /** One line per reason the plan cannot proceed; empty when it can. */
1313
+ reasons: string[];
1314
+ }
1315
+ interface ApprovalEvaluation {
1316
+ plan: ExecutionPlan;
1317
+ /** Absent falls back to {@link DEFAULT_APPROVAL_POLICY}. */
1318
+ policy?: ApprovalPolicyFile;
1319
+ /** Decisions collected so far. */
1320
+ grants?: readonly ApprovalGrant[];
1321
+ /**
1322
+ * The branch tip being presented for a `change` decision. When given, a
1323
+ * `change` grant must name this commit: a review of an older tip is not a
1324
+ * review of what is about to merge. When absent, `change` grants are matched
1325
+ * on `plan_hash` alone.
1326
+ */
1327
+ commit_sha?: string;
1328
+ /** ISO 8601 UTC. Passed in, never read from the clock. */
1329
+ now?: string;
1330
+ }
1331
+ /**
1332
+ * The class a plan is judged at: its declared risk, or its worst step's if
1333
+ * that is higher. A plan cannot understate itself — declaring `read_only`
1334
+ * while carrying a deploy step would otherwise walk straight past the policy.
1335
+ */
1336
+ declare function effectiveRisk(plan: ExecutionPlan): RiskClass;
1337
+ /**
1338
+ * The requirements a policy produces for a plan, before any decision is
1339
+ * collected. This is what a plan card renders: "this needs one reviewer on the
1340
+ * diff and two on release, because it deploys".
1341
+ *
1342
+ * Each matching rule contributes its own requirement and all of them must be
1343
+ * met. Merging two rules into one would need a rule for combining their modes,
1344
+ * roles and counts — and every such rule has a case where the result is
1345
+ * *looser* than one of its inputs. Keeping them separate is what makes the
1346
+ * policy additive in fact and not just in intent.
1347
+ *
1348
+ * `auto` rules are dropped rather than kept as zero-approver requirements: a
1349
+ * requirement nobody has to satisfy is noise on the card, and a stricter rule
1350
+ * matching the same gate still stands on its own.
1351
+ */
1352
+ declare function requiredApprovals(plan: ExecutionPlan, policy?: ApprovalPolicyFile): ApprovalRequirement[];
1353
+ /**
1354
+ * May this plan proceed?
1355
+ *
1356
+ * Answers with the full picture rather than a boolean: which requirements
1357
+ * exist, which are met, who met them, and — for every decision that was
1358
+ * supplied but did not count — why it did not. The last part is what makes
1359
+ * "I approved this, why is it still blocked?" answerable without reading the
1360
+ * evaluator's source.
1361
+ */
1362
+ declare function evaluateApproval(input: ApprovalEvaluation): ApprovalDecision;
1363
+ //#endregion
743
1364
  //#region src/provider.d.ts
744
1365
  /**
745
1366
  * A single file change within a plan.
@@ -1421,7 +2042,11 @@ declare const PATH_PATTERNS: {
1421
2042
  readonly config: ".contentrain/config.json";
1422
2043
  readonly context: ".contentrain/context.json";
1423
2044
  readonly vocabulary: ".contentrain/vocabulary.json";
1424
- readonly model: ".contentrain/models/{modelId}.json";
2045
+ readonly model: ".contentrain/models/{modelId}.json"; /** Detected source capabilities and the provider chosen for each — `CapabilityManifest`. */
2046
+ readonly capabilities: ".contentrain/capabilities.json"; /** Scheduled and event-driven agent work — `AutomationDefinition[]`. */
2047
+ readonly automations: ".contentrain/automations.json"; /** Which operations need whose approval — `ApprovalPolicyFile`. */
2048
+ readonly approvalPolicies: ".contentrain/approval-policies.json"; /** Source→destination URL map, the output of a migration's `moved` entries. */
2049
+ readonly redirects: ".contentrain/redirects.json";
1425
2050
  readonly content: {
1426
2051
  readonly singleton: ".contentrain/content/{domain}/{modelId}/{locale}.json";
1427
2052
  readonly collection: ".contentrain/content/{domain}/{modelId}/{locale}.json";
@@ -1437,6 +2062,30 @@ declare const PATH_PATTERNS: {
1437
2062
  readonly dictionary: ".contentrain/meta/{modelId}/{locale}.json";
1438
2063
  };
1439
2064
  };
2065
+ /**
2066
+ * Paths this repository reserves but does not yet write.
2067
+ *
2068
+ * Reserving them is the point. `.contentrain/` is a shared namespace: the
2069
+ * migration engine, Studio and a customer's own tooling all write into it. A
2070
+ * name claimed here cannot later be taken for something else, and — more
2071
+ * usefully — the tools that walk the directory know these four are *expected*
2072
+ * rather than stray.
2073
+ *
2074
+ * The contract for them is deliberately narrow and must stay that way until a
2075
+ * tool actually owns one:
2076
+ *
2077
+ * - `contentrain_doctor` and `contentrain_validate` ignore them. They are not
2078
+ * orphans, not broken content, and not the validator's business.
2079
+ * - `contentrain_reconcile` treats each as one opaque file: it takes the side
2080
+ * that changed, and reports `file_conflict` when both sides did. It never
2081
+ * merges their interiors, because it does not know their interiors.
2082
+ *
2083
+ * Both behaviours are pinned by tests in `@contentrain/mcp`, so a future
2084
+ * change to directory walking cannot quietly start flagging or auto-merging
2085
+ * these files.
2086
+ */
2087
+ declare const RESERVED_PATHS: readonly [".contentrain/capabilities.json", ".contentrain/automations.json", ".contentrain/approval-policies.json", ".contentrain/redirects.json"];
2088
+ declare function isReservedPath(path: string): boolean;
1440
2089
  /** Slug: lowercase alphanumeric with hyphens */
1441
2090
  declare const SLUG_PATTERN: RegExp;
1442
2091
  /** Entry ID: alphanumeric, 1-40 chars, starts with alphanumeric */
@@ -1734,10 +2383,6 @@ declare function validateAccept(value: string, accept: string): ValidationError[
1734
2383
  * or `maxSize` — byte length is not knowable from a stored path.
1735
2384
  */
1736
2385
  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
2386
  /** Generate a 12-character hex entry ID */
1742
2387
  declare function generateEntryId(): string;
1743
2388
  /**
@@ -1766,6 +2411,31 @@ declare function conflictId(conflict: {
1766
2411
  ours?: unknown;
1767
2412
  theirs?: unknown;
1768
2413
  }): string;
2414
+ /**
2415
+ * Read one frontmatter scalar: booleans, null, numbers, quoted strings with
2416
+ * their escapes decoded, or the text as written.
2417
+ *
2418
+ * Exported because it is a **contract**, not a utility. Two readers open these
2419
+ * documents — this package (through `parseMarkdownFrontmatter`, which
2420
+ * `@contentrain/mcp` re-exports) and `@contentrain/query`'s generator and Astro
2421
+ * loader. When each had its own copy they disagreed about what `"He said
2422
+ * \"Hi\""` says, which means the generated client and the content engine
2423
+ * disagreed about the same file. One implementation, imported by both.
2424
+ */
2425
+ declare function parseFrontmatterScalar(raw: string): unknown;
2426
+ /**
2427
+ * A scalar read as a string: quoting and escapes are resolved, but a value that
2428
+ * merely looks like a number or a boolean stays the text it was. Array items
2429
+ * use this, and so does any reader that knows from the model that a field is
2430
+ * string-typed — a SKU of `"007"` must not become the number 7.
2431
+ */
2432
+ declare function parseFrontmatterScalarString(raw: string): string;
2433
+ /**
2434
+ * Split an inline array's interior on commas that are not inside a quoted
2435
+ * item — `["a, b", c]` is two items, not three. Exported for the same reason
2436
+ * as {@link parseFrontmatterScalar}: both readers must split it the same way.
2437
+ */
2438
+ declare function splitFrontmatterList(inner: string): string[];
1769
2439
  /** Parse markdown frontmatter — extracts YAML frontmatter and body from a markdown string */
1770
2440
  declare function parseMarkdownFrontmatter(content: string): {
1771
2441
  frontmatter: Record<string, unknown>;
@@ -1774,5 +2444,5 @@ declare function parseMarkdownFrontmatter(content: string): {
1774
2444
  /** Serialize data + body into markdown frontmatter format */
1775
2445
  declare function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string;
1776
2446
  //#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 };
2447
+ 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
2448
  //# sourceMappingURL=index.d.mts.map