# ssp-ingest — phase detail and CHANGE-PLAN format

Full per-phase doctrine and the CHANGE-PLAN format spec for the six-phase ingest pipeline. The phase map, gates, site targeting, and output-folder layout live in `skills/ssp-ingest/SKILL.md`.

## Phase 1 — Classify

Read every file under `inputs/` (flat — no subfolders required). For each file, tag:

| Tag | Examples | How to detect |
|---|---|---|
| **kind** | `policy` \| `procedure` \| `transcript` \| `prior-ssp` \| `diagram` \| `ad-hoc` | Filename heuristics + first-page content scan. Transcripts have speaker turns; policies have section headers like "Purpose", "Scope", "Roles & Responsibilities"; prior SSPs match FedRAMP §1-15 structure; diagrams are image files (deferred for v1). |
| **scope** | `system_information` \| `Capability` \| `Component` \| `IR` \| `IS` \| `SP` \| `multi` | What this artifact primarily describes — IR (per-control row), IS (per-(Component × control) narrative leaf), and SP (a chosen ODP value, dtype 193) are distinct scopes. A statement that names a concrete value for a control's `[Assignment: organization-defined ...]` slot (e.g. "session timeout is 15 minutes", "password minimum length is 14") is `SP` scope, not `IS`. A single transcript often spans multiple scopes. |
| **candidate_capabilities** | `IAM`, `Logging & Monitoring`, … | Match against the seed list keywords + the tenant's tenant-specific Capabilities. |
| **candidate_components** | `Entra ID`, `Defender XDR`, … | Match against existing Components in Halo (via `asset_summary` filtered to dtype 173 for the id set, then `get_asset` per candidate). New tools surfaced here become candidate `Proposed New Component` rows in Phase 3. |
| **candidate_controls** | `AC-02`, `AC.L2-3.1.5`, … | Extract via regex against the baseline control IDs in scope (resolve via `lookup_controls` / `baseline_catalog` filtered to the Class baselines on `Site.CFBaselines`). |

Output: `outputs/ssp-ingest-{date}-{run-id}/classification.jsonl` (one row per input file).

## Phase 2 — Extract

For each classified file, pull structured evidence statements. The extractor's contract:

```
For each evidence statement:
  - text: verbatim or near-verbatim from source (single paragraph, ≤ ~400 chars; longer splits to multiple statements)
  - citation: file:line for documents | speaker@timestamp+filename for transcripts
  - claim_type: identity (Title/Description/Purpose) | implementation (how) | attribution (which control) | odp_value (a chosen organization-defined parameter value → Set Parameter) | narrative (system_information)
  - target_scope: which asset type this statement informs (incl. SP for ODP values)
  - target_identifier: AFUUID if the existing asset is known, otherwise a candidate identity (e.g. "Component:Entra ID", or "SP:AC-12-1" for an ODP slot — to be resolved in Phase 3)
  - controls: list of control IDs implicated, if any
```

**ODP value extraction (claim_type `odp_value`):** When a statement names a concrete value the organization has CHOSEN for a control's `[Assignment: organization-defined ...]` slot — a duration, count, frequency, list, role, or threshold tied to a specific control requirement — tag it `odp_value` / `target_scope: SP`. Capture the value verbatim into `text`, the implicated control(s) into `controls`, and (if discernible) whether the value is per-control or system-wide. Do NOT bury the value inside an `implementation` statement destined for an IS — the value belongs in a Set Parameter asset (Parameter Value, field 236), and the IS narrative describes only HOW the control is met.

Output: `outputs/ssp-ingest-{date}-{run-id}/evidence.jsonl` (one row per extracted statement).

**Transcript extraction specifics:**
- Group consecutive turns by speaker when they're on the same topic; each grouped block is one evidence statement.
- Citation format: `inputs/2026-05-12-iam-review.md @ 00:14:32 (J. Smith)` — speaker name in parentheses, timestamp in HH:MM:SS, filename relative to repo root.

**Document extraction specifics:**
- Citation format: `inputs/access-control-policy.md §L412-L418` — line range, section anchor optional.
- Paragraphs are the natural evidence unit. Lists become one statement per bullet only when the bullets express distinct controls.

**Prior-SSP extraction specifics:**
- Map by section: §9 narratives → `system_information`; §13 control responses → IS rows (one IS per `{Component, requirement-intent}`) and IR rows (one IR per Control in scope); §13 filled-in `[Assignment: organization-defined ...]` values → SP rows (one per ODP slot), NOT folded into the IS narrative.
- Preserve original IS statements verbatim when no contradicting evidence exists in other artifacts. Where a prior SSP renders ODP values inline in the control statement (the common case), lift each value out into a distinct `odp_value` statement so Phase 3 can reconcile it to a Set Parameter asset.

## Phase 3 — Reconcile

Diff every Phase 2 statement against live Halo state. For each:

1. **Read live state** — `system_information(site_id)`, plus `asset_summary` (type-agnostic; filter by dtype 178/173/190/191/193) scoped to the target Site for the id roll-up, then `get_asset` per matched row for full field state.
2. **Match by AFUUID first**, then by canonical identity ((Title) for Capability; (Title, Provider) for Component; (Catalog, Control-ID) for Implemented Requirement (dtype 190); (Component AFUUID, requirement-intent acronym) for Implementation Statement (dtype 191) — framework IDs are NOT part of IS identity, they're attached via framework CFs + parent IR rel-7 links; (Catalog, Control-ID, Parameter Index, Scope) for Set Parameter (dtype 193) — the value is NOT part of SP identity, an UPDATE changes the Parameter Value field 236 on the matched SP). All data follows the prefix convention (IR- for dtype 190, IS- for dtype 191, SP- for dtype 193) — no legacy-prefix fallback needed.
3. **Decide row type:**
   - `CREATE` — no match, identity is well-formed → add CREATE row. **Component/IS CREATE rows carry a library binding:** resolve the acronym against the definition libraries (`component_definitions_summary` / `implementation_statement_definitions_summary`) and annotate the row `library: <acronym or row id>` — or `library: off-library` when no definition exists (a flagged warning, not a halt; log the gap as a library-backfill candidate). The binding tells Phase 6 which write path to take and shows the user how much of the plan is library-sourced.
   - `UPDATE` — match found, evidence implies changes → add UPDATE row with field-level diffs.
   - `NO-OP` — match found, evidence consistent with existing state → drop from plan (still log in evidence.jsonl).
   - `MERGE-CANDIDATE` — two evidence statements appear to describe the same conceptual entity with different identities → flag for human resolution; do not auto-merge.
4. **Stage unmapped Components** as `PROPOSED-NEW` rows in a dedicated section of the CHANGE-PLAN. These are NOT auto-created during Phase 5 writes; the user approves the proposal in Phase 4, and only then does Phase 5 create + link them.

**The framework-spine shell is reconciler-owned, not document-driven.** Capabilities (from the `CTCapabilities` seed), Control Implementations, and Implemented Requirements (one per control in each baseline) are catalog-derived and materialized by `reconcile_ssp_assets` in Phase 6 — do NOT emit bare `CREATE Capability / Control Implementation / Implemented Requirement` shell rows from the documents. What the documents drive against the spine is *narrative and chosen values*: plan Capability Descriptions and IR Description rollups as `UPDATE` rows against the reconciler-created assets, and route a genuinely new Capability to the `CTCapabilities` seed list (per [[ssp-capability-seed-list]]) rather than a per-run create. The diff still drives `CREATE`/`UPDATE`/`NO-OP` decisions for the document-owned types: `system_information`, Components, Implementation Statements, and **Set Parameters**.

**Set Parameter reconciliation.** Each `odp_value` evidence statement reconciles to a Set Parameter asset (dtype 193). Match an existing SP by (Catalog, Control-ID, Parameter Index, Scope); `CREATE` a new SP when the ODP slot has no value yet, `UPDATE` when the chosen value changed (the diff is on Parameter Value, field 236), `NO-OP` when consistent. Determine the OSCAL placement from the evidence: a value scoped to one control parents the SP to the IR (rel 12, Scope=`Control`); a value declared once for the whole system parents to the CI (rel 11, Scope=`System`). Stage the parent linkage in the plan's relationships section. Where a FedRAMP Rev5 control's catalog requirement has an ODP slot but the documents supply no value, surface it as a gap (an unfilled-ODP row), not a fabricated SP — never invent an ODP value.

Output: `outputs/ssp-ingest-{date}-{run-id}/change-plan.md` (the human-readable plan) and `outputs/ssp-ingest-{date}-{run-id}/change-plan.jsonl` (the machine-readable form Phase 5 consumes).

## Phase 5 — User-edit loop (outer)

Render the polished CHANGE-PLAN inline in chat, including the Reviewer Notes summary. Then pause with:

```
Approve and write to Halo? Or describe edits you want, and I'll re-render.
```

User response shapes:

- **Approve** (`approve` / `yes` / `apply` / `looks good`) → exit the outer loop, proceed to Phase 6 writer.
- **Cancel** (`no` / `cancel` / `abort`) → exit the outer loop, run aborts with no writes.
- **Edit feedback in any natural shape** — examples:
  - "tighten the Defender XDR statement on row 31"
  - "row 5's Origination should be Inherited, not Organization"
  - "split the IAM statement into two IRs covering AC-02 and AC-03 separately"
  - "the Capability description for Logging is too generic — say it covers SIEM + audit aggregation"
  - "drop row 73"
  - "Microsoft - GCC High everywhere, not just some rows"
  - "Entra ID, not Azure AD"

On edit feedback:
1. Translate the user's natural language into concrete CHANGE-PLAN row mutations (text edits, structural field changes, row deletes / adds / splits).
2. Apply the mutations.
3. **Re-run Phase 4 (internal review loop) against the modified plan** — user edits can introduce BLOCKERs that need surfacing.
4. Re-render the new polished draft inline. Log this iteration in `outputs/ssp-ingest-{date}-{run-id}/user-iterations.md`.
5. Pause again with the same prompt. Loop until user approves or cancels.

**No write tool fires until the user has explicitly approved the current iteration's draft.**

## Phase 6 — Compose → Apply

Phase 6 no longer hand-applies the plan via direct MCP calls. The approved CHANGE-PLAN rides the [[halo-action-pipeline]]: deterministic compile + a residual compose step produce action files, `/grcitsm-analyst:halo-apply` is the only thing that writes, and the reconciler-owned steps run as direct main-thread MCP calls at their spine positions (per [[halo-ssp-asset-write-conventions]] Rule 5) — never inside an action file. Phase 6 only fires once the user has explicitly approved the final iteration of the Phase 5 outer loop; no compose happens before approval.

The command (main thread) orchestrates this sequence:

```
1. reconcile_ssp_assets                    BULK, direct MCP — creates missing Capabilities + Control
                                           Implementations + Implemented Requirements + framework CFs.
                                           Runs FIRST so Capability/CI/IR AFUUIDs resolve in step 2's snapshot.
2. Snapshot + library CSVs                 read-only agent step (asset_summary/get_asset scoped to the Site;
                                           library lookups), exactly as /grcitsm-analyst:ssp-compile step 2 —
                                           or a user-exported CSV for bulk scopes.
3. COMPILE — ssp_compile.py                over manifests/ + companions/. The manifests MUST match the
                                           APPROVED plan: if Phase-5 edits touched structural rows, or the
                                           run is interview-driven (no Phase-3 projection), the main agent
                                           first re-projects the approved change-plan.jsonl's structural
                                           rows into manifests/ (same column contract, [[ssp-manifest-schemas]]).
                                           Compile emits →
                                           outputs/<run>/state/actions-01.jsonl (+ plan-review.csv, report.json).
                                           Owns ALL structure: Component/IS/SP creates+updates, rel 8,
                                           library-first payloads, three-phase framework CFs, AFUUID guards.
                                           Fail-loud rules + drift/no-op/orphan handling: [[ssp-csv-compile]].
4. COMPOSE — ssp-writer agent              composes the residual approved rows the manifests cannot carry →
                                           outputs/<run>/state/actions-02.jsonl: system_information patch
                                           (full merged row), non-manifest UPDATE rows, approved
                                           MERGE-CANDIDATE resolutions (UPDATE on target), IR Description
                                           rollups (plan-derived, IR ids from the step-2 snapshot).
                                           Never calls a Halo write tool.
5. APPLY — /grcitsm-analyst:halo-apply     actions-01.jsonl then actions-02.jsonl, in order; executor fleet,
                                           dependency order via needs edges, append-only state/progress.jsonl
                                           ledger, guarded creates search-or-skip.
6. Relationship reconcilers                BULK, direct MCP, after apply completes (IS framework CFs from
                                           step 5 are the match keys): reconcile_ssp_relationships_ci_ir
                                           (rel 10) → _ir_is (rel 7) → _ir_sp (rel 12) → _ci_sp (rel 11).
7. VERIFY pass                             sample get_asset read-backs (≥10% of applied writes; 100% for runs
                                           <10) per [[halo-ssp-asset-write-conventions]] Rule 7 — "Task queued"
                                           is never confirmation. Project uuid-map.jsonl from the ledger's
                                           done events (AFUUID → asset_id for future runs). Report the ledger
                                           summary. A verify failure halts with a marker file naming the
                                           asset_id and field.
```

Row-class routing (compile vs compose):

- **Framework-spine shell (Capabilities / CIs / IRs + rel 6/10/7/12/11):** reconciler-owned (steps 1 and 6) — never emitted as actions; `ssp_compile.py` asserts this (`assert_no_reconciler_output`), and the composer never emits them either. The plan's Capability/IR entries represent reconciler-applied shell plus composed narrative (Capability Description UPDATEs, IR Description rollups), not creates.
- **CREATE rows (Component / IS / SP) + rel 8 + all manifest-carried UPDATE fields** (rel 6 is reconciler-owned — validated in the manifest, never emitted)**:** compiler-owned via the Phase-3 manifests — library-first resolution, three-phase CFs, and spine emission order are encoded by `ssp_compile.py` ([[ssp-csv-compile]]); nothing restates them here.
- **Residual UPDATE rows / system_information / merge resolutions / IR rollups:** composer-owned (`agents/ssp-writer.md`). Multi-select merges are computed at compose time from the snapshot (or targeted `get_asset`) so the action carries the full merged array.
- **NO-OP rows:** filtered in Phase 3 (and the compiler re-skips unchanged rows against a wide snapshot) — assert and drop if any slip through.
- **MERGE-CANDIDATE rows:** must be resolved by Phase 5 approval; an unresolved one halts the run before compose.

Resume: the pipeline ledger is the single resume surface — re-run `/grcitsm-analyst:halo-apply` against the same run folder with `doneIds` seeded from `state/progress.jsonl`; reconcilers are idempotent and safe to re-run. The former `applied.jsonl` writer ledger is retired.

Output: `outputs/ssp-ingest-{date}-{run-id}/state/actions-*.jsonl` + `state/progress.jsonl` (the write ledger) + `uuid-map.jsonl` (projected from the ledger — read by future ingest runs to re-resolve identity).

## CHANGE-PLAN format

A single markdown file rendered inline in chat AND saved to `outputs/ssp-ingest-{date}-{run-id}/change-plan.md`. Structure:

```markdown
# SSP Ingest CHANGE-PLAN
**Run:** 2026-05-25-r42
**Site:** <site-id> (Acme Corp / ACME)
**Inputs:** 8 files (3 policies, 4 transcripts, 1 prior SSP)

## Summary

| Action | Count |
|---|---|
| CREATE Capability | 4 |
| CREATE Component | 0 (PROPOSED-NEW: 2) |
| CREATE Implemented Requirement | 12 |
| CREATE Implementation Statement | 18 |
| CREATE Set Parameter | 9 |
| UPDATE system_information | 3 fields |
| UPDATE Capability | 0 |
| UPDATE Component | 6 |
| UPDATE Implemented Requirement | 4 |
| UPDATE Implementation Statement | 22 |
| UPDATE Set Parameter | 2 |
| UNFILLED-ODP (gap, no value) | 3 |
| MERGE-CANDIDATE | 1 |

## system_information (Site <site-id>)

| # | Field | Old → New | Source |
|---|---|---|---|
| 1 | General System Description | "" → "The ACME platform is built on…" | inputs/system-overview.md §L1-L42 |
| 2 | Authorization Boundary Description | "{existing text…}" → "{updated text…}" | inputs/2026-05-12-architecture-review.md @ 00:23:14 (M. Chen) |
| 3 | Data Flow Description | "" → "All customer data resides within AWS…" | inputs/system-overview.md §L44-L78 |

## Capabilities

| # | Action | Title | Description | Framework CFs | Source |
|---|---|---|---|---|---|
| 4 | CREATE | Identity & Access Management | "Authentication, authorization, federation, SSO, MFA…" | fedramp_controls: AC-02,AC-03,IA-02 | inputs/access-control-policy.md §L1-L20 |
| 5 | CREATE | Logging & Monitoring | "Audit log generation, aggregation…" | fedramp_controls: AU-02,AU-06,AU-12 | inputs/audit-logging-procedure.md §L1-L30 |
| 6 | CREATE | Endpoint Protection | "EDR/XDR, MDM, MAM…" | fedramp_controls: SI-03,SC-07 | inputs/2026-05-12-endpoint-review.md @ 00:08:42 (J. Smith) |
| 7 | CREATE | Incident Response | "IR plan, SOC, tabletops…" | fedramp_controls: IR-04,IR-06 | inputs/2026-05-12-ir-review.md @ 00:14:32 (J. Smith) |

## Components

(no CREATE rows in this run — see PROPOSED-NEW section below)

| # | Action | Asset ID | Title | Field | Old → New | Source |
|---|---|---|---|---|---|---|
| 8 | UPDATE | 124 | Defender XDR | Purpose | "{old}" → "{new}" | inputs/2026-05-12-endpoint-review.md @ 00:11:08 (J. Smith) |
| 9 | UPDATE | 125 | Huntress ITDR | Provider | "" → "Huntress" | inputs/vendor-list.md §L18 |
| … | | | | | | |

## Implemented Requirements (dtype 190 — per-control)

| # | Action | Asset ID | AFUUID | Title | Field | Old → New | Source |
|---|---|---|---|---|---|---|---|
| 20 | CREATE | (new) | IR-ACME-FR5CC-AC-02 | "AC-02: Account Management" | Description | "" → "Account-management lifecycle owned by the IAM Capability…" | inputs/access-control-policy.md §L1-L40 |
| 21 | CREATE | (new) | IR-ACME-FR20XCC-IAM-AAM | "KSI-IAM-AAM: Automating Account Management" | Description | "" → "20x KSI family covering account-management automation…" | inputs/20x-overview.md §L8-L18 |

## Implementation Statements (dtype 191 — per Component × control)

| # | Action | Asset ID | Title | Field | Old → New | Source |
|---|---|---|---|---|---|---|
| 30 | CREATE | (new) | "Entra ID – Account types defined and documented for each identity class" | Implementation Statement | "" → "Entra ID defines and documents account types in scope for…" | inputs/access-control-policy.md §L102-L118 |
| 31 | UPDATE | 143 | "All CUI Components – Physical access restrictions reviewed at change events" | Implementation State | "Implemented" → "Partial" | inputs/2026-05-12-physical-review.md @ 00:34:17 (R. Patel) |
| 32 | UPDATE | 143 | "All CUI Components – Physical access restrictions reviewed at change events" | Responsible Role | "Cloud Service Provider" → "Self" | inputs/2026-05-12-physical-review.md @ 00:34:51 (R. Patel) |
| … | | | | | | |

## Set Parameters (dtype 193 — OSCAL set-parameter / ODP values)

| # | Action | Asset ID | AFUUID | Control | Label / Index | Scope | Parameter Value (Old → New) | Source |
|---|---|---|---|---|---|---|---|---|
| 40 | CREATE | (new) | SP-ACME-FR5CC-AC-12-1 | AC-12 | "session inactivity period" / 1 | Control | "" → "15 minutes" | inputs/access-control-policy.md §L210-L214 |
| 41 | CREATE | (new) | SP-ACME-FR5CC-IA-05-1 | IA-05 | "password minimum length" / 1 | Control | "" → "14 characters" | inputs/password-policy.md §L8-L12 |
| 42 | UPDATE | 612 | SP-ACME-FR5CC-AU-11-1 | AU-11 | "audit record retention period" / 1 | Control | "90 days" → "1 year" | inputs/2026-05-12-logging-review.md @ 00:22:40 (J. Smith) |

## Unfilled ODP slots (gap — no value supplied)

| # | Control | ODP slot | Why no value | Source |
|---|---|---|---|---|
| 45 | AC-02(03) | "time period of inactivity for account disable" | FedRAMP Rev5 catalog defines the ODP but no input artifact supplies a value | (no source — surfaced from catalog) |

## Asset relationships

| # | Action | Upstream | Downstream | Relationship |
|---|---|---|---|---|
| 50 | LINK | Capability "IAM" (new) | Component 124 (Entra ID) | Capability Has Components (rel 6) |
| 51 | LINK | Capability "IAM" (new) | Component 130 (PIM) | Capability Has Components (rel 6) |
| 52 | LINK | Component 124 (Entra ID) | IS (new, row 30) | Component Has Implementation Statements (rel 8) |
| 53 | LINK | IR-ACME-FR5CC-AC-02 (new, row 20) | IS (new, row 30) | Implemented Requirement Has Implementation Statements (rel 7) |
| 54 | LINK | IR-ACME-FR20XCC-IAM-AAM (new, row 21) | IS (new, row 30) | Implemented Requirement Has Implementation Statements (rel 7) |
| 55 | LINK | IR-ACME-FR5CC-AC-12 | SP-ACME-FR5CC-AC-12-1 (new, row 40) | Implemented Requirement Has Set Parameters (rel 12, Scope=Control) |
| 56 | LINK | CI-ACME-FR5CC | SP-ACME-FR5CC-AU-11-1 (row 42) | Control Implementation Has Set Parameters (rel 11, Scope=System) |
| … | | | | |

## PROPOSED-NEW (require approval to create + parent)

| # | Asset type | Title | Why proposed | Evidence count | Source(s) |
|---|---|---|---|---|---|
| 60 | Component | Splunk Cloud | Mentioned in 4 evidence statements as the primary log destination; no existing Component matches | 4 | inputs/audit-logging-procedure.md §L31-L48; inputs/2026-05-12-logging-review.md @ 00:18:22 (J. Smith) |
| 61 | Capability | Continuous Compliance Automation | 3 evidence statements describe a CSM-as-Component-of-implementations program that doesn't fit any seeded Capability | 3 | inputs/continuous-monitoring-plan.md §L1-L18 |

## MERGE-CANDIDATE (require user resolution)

| # | Candidate A | Candidate B | Why ambiguous |
|---|---|---|---|
| 70 | Component "Entra ID" (asset 124, Provider="Microsoft - GCC High") | Component "Entra" (proposed-new from inputs/transcript-3.md @ 00:42:11) | Same product likely; transcript uses shortened name and no Provider |

---

**Apply this CHANGE-PLAN? (yes / no / approve rows X,Y,Z only)**
```

Approving "rows 1-7, 30, 50-51" applies just those rows; everything else is skipped.

## Failure modes

| Symptom | Cause | Response |
|---|---|---|
| `/ssp-ingest` with no flags emits "No Site specified" | Site targeting requirement | Add `--site-id N` or `--client-name "X"` and re-run. |
| CHANGE-PLAN has no rows after a non-empty inputs/ | Classifier filtered everything; possibly inputs are all images or all unrecognized formats | Inspect `classification.jsonl`; manually adjust file extensions or add `.meta.json` sidecars to declare kind. |
| Apply verify reads cleared fields after `"Task queued"` | A composed action re-sent a multi-select without its existing members (compose-time merge miss) | Abort and recover from Halo's change history (it logs the prior values). Fix the composed action's merged array and re-apply; file an issue against the composer logic. |
| Framework CFs empty after apply | Three-phase ordering broken — a Requirements-tier action ran before its Controls-tier `needs` completed | Should be structurally impossible (needs edges); if seen, inspect `state/progress.jsonl` for an out-of-order event, then re-apply the affected CF actions. See [[halo-ssp-asset-write-conventions]] § Rule 3. |
| User approves rows including a PROPOSED-NEW Component but the apply skips the IS rows that depend on it | Spine order violation — IS rows need their parent Component to exist first | Re-run with the PROPOSED-NEW row included in the manifests; the compiler emits the Component create before the IS with a `needs` edge. |
| Site has multiple baselines (e.g. CMMC L2 + FedRAMP Rev5 Class C) and a transcript references controls from both | Normal — an IS can satisfy multiple IRs across baselines via parallel rel-7 links | No action; the IS row gets one IR parent per catalog plus the framework CFs include all relevant control IDs. |
| Lookup-value not in the field-discovery `[Allowed Values]` | Static lookup: Halo will silently drop it. Framework CFs and baselines are DYNAMIC lookups — their `[Allowed Values]` snapshot can be stale, so absence there does not mean the value is invalid | Pre-validate every CF value during Phase 3. Static-lookup fields: validate against `asset_type_fields` `[Allowed Values]`; reject the row before Phase 4 with a fix suggestion. Dynamic-lookup fields: validate against the per-family catalog tool instead — `lookup_controls` / `lookup_requirements` for framework CF values, `baseline_catalog` `[Baseline Name]` for baseline names — all catalog rows count, active or inactive: prefer `Active == True` rows for NEW baseline assignments, but a name matching an inactive baseline is valid and must not be rejected (historical records keep retired baselines); never reject a row solely because a dynamic value is absent from `[Allowed Values]`. See `halo-platform/write-semantics.md` § 3.11. |
