Skip to content

Latest commit

 

History

History
18 lines (12 loc) · 5.35 KB

File metadata and controls

18 lines (12 loc) · 5.35 KB

User-submission tables get their own carry/empty gate, default empty

The classifier's four operational categories (skills/mkwp/scripts/classify.py's OPERATIONAL_TABLE_PATTERNS — analytics, cookie-consent, email-log, search-index) are all locally regenerable: nothing of value is lost by carrying them empty, so they are folded silently into the db_table_content recommendation with no gate of their own. Form-submission tables (wsf_submit, fluentform_submissions, frm_items, wpforms_entries, gf_entry, and their meta/detail siblings) are a different case entirely: they are neither regenerable nor operational — a lost submission is gone for good — and they are the most privacy-sensitive data the transfer handles: real names, email addresses, and free-text messages from site visitors. Spreading that onto a developer's laptop by default is a GDPR-relevant decision the operator should make deliberately, not one the classifier should fold into the same silent-empty bucket as an analytics cache.

So user_submissions is a fifth classification family, matched the same way as the operational four (after prefix-stripping, first match wins — ADR-0005's pattern), but kept structurally separate in classify.py and given its own standalone decision in resolve_plan.py: a plain carry/empty gate, default empty, sitting immediately after db_table_content in the ordered decision list. Regenerable-vs-non-regenerable is what separates it from the operational four; non-regenerable-vs-privacy-sensitive is what separates it from every other carried-in-full content table (posts, users, CRM/forms configuration) — those are also irreplaceable, but they are not routinely full of third-party personal data the way a contact-form inbox is.

Default empty is privacy minimisation: the common case is a developer who needs a working local copy to build and test against, not a copy of every visitor's submitted phone number. The gate is the way back — an operator debugging a form-submission bug that only reproduces with real entries can accept carry for that site, and the choice persists in the saved plan (.kntnt-wp-skills.json under user_submissions) and replays with every other settled decision, so it is a one-time choice per site rather than a repeated interruption.

The operator settled this policy in-session 2026-07-19, with explicit authority granted to record it here.

Consequences

  • classify.py's table_category() tags a matched table user_submissions, distinct from the four operational categories, even though the underlying table still lands in the same full/empty table split structure — the distinction that matters downstream is the category tag, not a different output shape.
  • The gate is always presented (or walked in replay), independent of whether the discovered site actually has any user-submission tables — the same posture media_originals and heavy_blobs already take for decisions that may be a no-op on a given site.
  • A saved carry is a deliberate, sticky per-site choice: it is not re-evaluated against live state on replay, mirroring every other coarse gate in the backbone except mail's mass-send valve, which has its own narrow safety exception (ADR-0009).
  • A resolved carry has to change what is actually dumped, not just add a value to the plan JSON: resolve_plan.py folds the gate's resolved choice into the db_table_content decision itself — moving every user_submissions-tagged table from that decision's empty list into its full-data list — so the pack script's content/empty table lists and the dump-sanity empty-set, which read db_table_content and never the user_submissions value directly, see the carry. The fold is applied to both the resolved value (this run's actual choice) and the recommendation (the saved/live/built-in layers, never the this-run answer), so a db_table_content gate walked before the operator answers user_submissions still shows a split consistent with whatever is already on record for the site.
  • The initial pattern set (WS Form, Fluent Forms, Formidable, WPForms, Gravity Forms) was checked against each plugin's actual schema before finalising per-plugin table names; the check caught one gap, since closed: Gravity Forms' save-and-continue drafts land in their own gf_draft_submissions table, outside the gf_entry prefix the rest of that family shares, and now has its own pattern entry. A form plugin outside this set is not yet recognised and its submissions are carried in full like any other content table until a pattern is added.
  • table_category() matches on the table name after the site's own prefix is stripped, not after any multisite sub-site prefix — a sub-site table such as wp_2_wsf_submit (WordPress multisite's per-site table prefix inserted between the base prefix and the table name) does not match any pattern and is carried in full. This mirrors the pre-existing limitation of the four operational patterns and is currently moot: multisite is out of scope for the whole engine (docs/spec.md, Out of Scope). Recorded here because the privacy rationale for this ADR would apply with equal force to a sub-site's form entries were multisite ever brought into scope, so that future work starts from an informed baseline rather than rediscovering the gap.