From 284423f9fd6e9f31bb3432ecf43bb1326d0578c0 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 12 Apr 2026 18:05:09 -0600 Subject: [PATCH 001/103] feat: Initializing transmission schema --- .../transmission/transmission_schema.json5 | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 compass/extraction/transmission/transmission_schema.json5 diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 new file mode 100644 index 000000000..30ff2a24c --- /dev/null +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -0,0 +1,25 @@ +{ + "title": "Transmission Ordinance Extraction Schema", + "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all r elevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", + "version": "1.0.0", + "type": "object", + "required": ["outputs"], + "additionalProperties": false, + "properties": { + "outputs": { + "type": "array", + "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per feature**. Never infer, imply, or guess a requirement from related context.", + "items": { + "type": "object", + "required": [ + "feature", + "value", + "units", + "section", + "summary" + ], + "additionalProperties": false, + } + } + } +} From 42b9f71af80a965287e99320e9955932f66fe363 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 12 Apr 2026 18:09:18 -0600 Subject: [PATCH 002/103] feat: Initializing some properties --- .../transmission/transmission_schema.json5 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 30ff2a24c..9f1ce5371 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -19,6 +19,18 @@ "summary" ], "additionalProperties": false, + "properties": { + "feature": { + "type": "string", + "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs.", + "enum": [ + "noise", + "maximum height", + "color", + "lighting", + ] + } + } } } } From 99c29231448d643a48a92deb6f3c21ae14e72493 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 12 Apr 2026 18:13:52 -0600 Subject: [PATCH 003/103] Defining `value` --- .../extraction/transmission/transmission_schema.json5 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 9f1ce5371..889dbd948 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -29,7 +29,16 @@ "color", "lighting", ] - } + }, + "value": { + "description": "The extracted ordinance value.", + "anyOf": [ + {"type": "number"}, + {"type": "string"}, + {"type": "array", "items": {"type": "string"}, "additionalProperties": false}, + {"type": "null"} + ] + }, } } } From 1ca43a2ab351004aba0f44165428c45c558a3b55 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 12 Apr 2026 19:53:22 -0600 Subject: [PATCH 004/103] Adding some qualitative_features --- compass/extraction/transmission/transmission_schema.json5 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 889dbd948..94b564cb5 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -43,4 +43,10 @@ } } } + "$qualitative_features": [ + "color", + "decommissioning", + "lighting", + "signage", + ] } From a7ab92bb566b3229c624a4137a1ededf2a729fef Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 19:43:54 -0600 Subject: [PATCH 005/103] typo: --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 94b564cb5..7894ae156 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -1,6 +1,6 @@ { "title": "Transmission Ordinance Extraction Schema", - "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all r elevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", + "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all relevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", "version": "1.0.0", "type": "object", "required": ["outputs"], From cad9214d1b3e3e8b5ef083175a030ae2d5316563 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 19:58:35 -0600 Subject: [PATCH 006/103] Complete and improved required fields --- .../transmission/transmission_schema.json5 | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7894ae156..66f6028f9 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -16,29 +16,48 @@ "value", "units", "section", - "summary" + "summary", + "explanation" ], "additionalProperties": false, "properties": { "feature": { "type": "string", - "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs.", + "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", "enum": [ - "noise", - "maximum height", - "color", - "lighting", + "noise" ] }, "value": { - "description": "The extracted ordinance value.", + "description": "The extracted ordinance value. For numerical setbacks/limits, use a number. For categorical outcomes, use a string. For fields that list multiple named items, use an array of strings. Use null only for qualitative features, and only when an enacted, explicit, enforceable ordinance requirement for that feature is present. Null must never be used to indicate absence. If a feature has no enacted, explicit requirement in the ordinance text, omit that feature from outputs.", "anyOf": [ {"type": "number"}, {"type": "string"}, - {"type": "array", "items": {"type": "string"}, "additionalProperties": false}, + { + "type": "array", + "items": {"type": "string"}, + "additionalProperties": false + }, {"type": "null"} ] }, + "units": { + "type": ["string", "null"], + "description": "Units for the extracted value. Preserve the ordinance unit wording exactly whenever possible. For setbacks/depth, use linear units such as 'feet' or 'meters' as stated in text. For noise, use 'dBA' only if the ordinance says 'dBA' or 'dB(A)'; if it says 'dB' without A-weighting, keep 'dB'. Use null for qualitative fields without measurable units." + }, + "section": { + "type": ["string", "null"], + "description": "The section title or number from the ordinance where this requirement appears. Include numerical labels if provided. Null if no section identifier is available." + }, + "summary": { + "type": "string", + "description": "A short summary of the relevant ordinance requirement using direct text excerpts and quotes as much as possible. If multiple options exist and a selection was made, list all other options and their conditions in the summary. For qualitative restrictions, this is the primary output field containing the full extracted text. Can be null if no requirement found." + //"description": "A short summary with direct ordinance excerpts/quotes whenever possible. For qualitative features (definitions, permitting, screening, inspection, decommissioning, prohibitions), this is the primary output field and should contain a direct ordinance excerpt. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders (for example, 'No explicit ... found'); omit the feature instead when no requirement is present." + }, + "explanation": { + "type": "string", + "description": "Brief rationale explaining why this row matches the selected feature under this schema. Reference the specific evidence in summary and how it supports the extracted value and units or, for qualitative features, the inclusion criteria. Must be a non-null, non-empty string and must not use absence placeholders." + } } } } From d3aa71261769ae32fb352ba4448149f3447f18a1 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 20:13:17 -0600 Subject: [PATCH 007/103] Simplified summary request There is some sensitivity on the summary requirements. Some of the wording used in other technologies can make it miss completely the field. --- compass/extraction/transmission/transmission_schema.json5 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 66f6028f9..4f0c642ff 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -51,7 +51,12 @@ }, "summary": { "type": "string", - "description": "A short summary of the relevant ordinance requirement using direct text excerpts and quotes as much as possible. If multiple options exist and a selection was made, list all other options and their conditions in the summary. For qualitative restrictions, this is the primary output field containing the full extracted text. Can be null if no requirement found." + "description": "A short summary of the relevant ordinance requiremets." + /* + Byron's + "description": "A short summary with direct ordinance excerpts or quotes whenever possible. For qualitative features such as permitting, fencing, lighting, seismic monitoring, decommissioning, and prohibitions, this is the primary output field and should contain direct ordinance language. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders such as 'No explicit requirement found'; omit the feature instead when no requirement is present." + */ + // "description": "A short summary of the relevant ordinance requirement using direct text excerpts and quotes as much as possible. If multiple options exist and a selection was made, list all other options and their conditions in the summary. For qualitative restrictions, this is the primary output field containing the full extracted text. Can be null if no requirement found." //"description": "A short summary with direct ordinance excerpts/quotes whenever possible. For qualitative features (definitions, permitting, screening, inspection, decommissioning, prohibitions), this is the primary output field and should contain a direct ordinance excerpt. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders (for example, 'No explicit ... found'); omit the feature instead when no requirement is present." }, "explanation": { From 84554c53ef63231f12da4b1c8313edbd8432c3a1 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 22:03:34 -0600 Subject: [PATCH 008/103] Defining noise --- .../extraction/transmission/transmission_schema.json5 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 4f0c642ff..0c0d45a8d 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -66,6 +66,16 @@ } } } + }, + "$definitions": { + "numerical_features": { + "description": "Non-setback numerical restriction features. Only extract if numerical values are explicitly given in the text.", + "properties": { + "noise": { + "description": "Extract maximum allowable operational noise for electric transmission lines and related facilities only when an explicit numeric limit is stated. Normalize A-weighted units to 'dBA' in units and preserve verbatim wording in summary. If the ordinance only references compliance with external standards or provides no numeric noise limit, omit this feature entirely." + } + } + } } "$qualitative_features": [ "color", From b4bb8a3ee998875ffa56d199649995ded2f5226f Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 22:11:22 -0600 Subject: [PATCH 009/103] examples of noise --- .../transmission/transmission_schema.json5 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 0c0d45a8d..f8c3f2f9e 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -76,11 +76,15 @@ } } } - } - "$qualitative_features": [ - "color", - "decommissioning", - "lighting", - "signage", - ] + }, + "$examples": [ + { + "feature": "noise", + "value": 50, + "units": "dBA", + "section": "SECTION 1308 – Performance / Construction Standards", + // Need some editing + "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance." + } + ], } From f7ee50b8a57851375acedb0943aa70efc1a48358 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 22:15:04 -0600 Subject: [PATCH 010/103] Adding instructions - general --- compass/extraction/transmission/transmission_schema.json5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index f8c3f2f9e..532da480d 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -87,4 +87,9 @@ "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance." } ], + "$instructions": { + "general": [ + "Use direct text excerpts and quotes in summary whenever possible.", + ] + } } From af96e8ff0530b2cbdd75af52daf56eab2c736411 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 13 Apr 2026 22:34:17 -0600 Subject: [PATCH 011/103] Adding decomissioning as an operational feature --- .../transmission/transmission_schema.json5 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 532da480d..7ae89a961 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -25,7 +25,8 @@ "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", "enum": [ - "noise" + "noise", + "decommissioning", ] }, "value": { @@ -75,7 +76,15 @@ "description": "Extract maximum allowable operational noise for electric transmission lines and related facilities only when an explicit numeric limit is stated. Normalize A-weighted units to 'dBA' in units and preserve verbatim wording in summary. If the ordinance only references compliance with external standards or provides no numeric noise limit, omit this feature entirely." } } - } + }, + "operational_features": { + "description": "Operational, licensing, permitting, inspection, and abandonment requirements. These features require text extraction only.", + "properties": { + "decommissioning": { + "description": "Extract requirements for abandonment, plugging, sealing, removal, and site restoration when systems are retired, fail, or wells are abandoned, including responsible party and timeline details when explicitly stated." + } + } + }, }, "$examples": [ { @@ -91,5 +100,8 @@ "general": [ "Use direct text excerpts and quotes in summary whenever possible.", ] - } + }, + "$qualitative_features": [ + "decommissioning", + ] } From 85f688cfc835022f6b8bfb591983ff22ad8f04cb Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 14 Apr 2026 09:36:37 -0600 Subject: [PATCH 012/103] feat: Adding Maximum Height Need some improvements to avoid confusion with facilities. --- compass/extraction/transmission/transmission_schema.json5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7ae89a961..04683f781 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -25,6 +25,8 @@ "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", "enum": [ + // Tower vs facility height. Do I want both? + "maximum height", "noise", "decommissioning", ] @@ -74,6 +76,9 @@ "properties": { "noise": { "description": "Extract maximum allowable operational noise for electric transmission lines and related facilities only when an explicit numeric limit is stated. Normalize A-weighted units to 'dBA' in units and preserve verbatim wording in summary. If the ordinance only references compliance with external standards or provides no numeric noise limit, omit this feature entirely." + }, + "maximum-height": { + "description": "Extract maximum structure height allowed." } } }, From eebbb05ac8da398fb8c9253385371271eaba771b Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 14 Apr 2026 18:15:21 -0600 Subject: [PATCH 013/103] feat: Property line setback --- .../transmission/transmission_schema.json5 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 04683f781..96703da90 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -25,6 +25,7 @@ "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", "enum": [ + "property lines", // Tower vs facility height. Do I want both? "maximum height", "noise", @@ -71,6 +72,14 @@ } }, "$definitions": { + "setback_features": { + "description": "Setback features for electrical transmission lines and related infrastructure. Treat each setback feature independently and do not cross-apply a setback unless the ordinance text explicitly states that it applies to multiple target types. When a single clause explicitly lists multiple target types and one shared numeric setback, emit one row per explicitly listed feature using the same numeric value and units and cite the same clause in summary. Apply the shared numeric prioritization rules in $core_principles when multiple numeric values explicitly apply to the same feature.", + "properties": { + "property lines distance": { + "description": "Minimum required separation from explicit front yard, side yard, rear yard, or similar yard-based setback areas. Extract this feature only when the ordinance states the electric transmission line and related facilities must meet a yard setback or be located within/outside a named yard area. Do not remap yard setbacks to 'property lines' unless the ordinance explicitly measures the requirement from the property line itself rather than from a yard classification. IGNORE: Do not respond based on generic property-line, road, or right-of-way setbacks unless the ordinance explicitly frames the requirement as a yard setback." + }, + } + }, "numerical_features": { "description": "Non-setback numerical restriction features. Only extract if numerical values are explicitly given in the text.", "properties": { @@ -104,6 +113,12 @@ "$instructions": { "general": [ "Use direct text excerpts and quotes in summary whenever possible.", + ], + "setbacks": [ + "Setbacks should be extracted as minimum separation distances.", + "Prefer numeric values with units ('feet', 'meters').", + "Setback rows must contain numeric value and non-null units; never emit qualitative-only setback rows.", + "Treat property-line, lot-line, and parcel-boundary setbacks as 'property lines' when the ordinance measures the setback from that boundary.", ] }, "$qualitative_features": [ From eacfef977d00747f64381f9ac8ee74faf59d0239 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Apr 2026 22:27:38 -0600 Subject: [PATCH 014/103] feat: core_principles It's incomplete. --- .../transmission/transmission_schema.json5 | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 96703da90..25cd64e14 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -71,6 +71,26 @@ } } }, + "$core_principles": { + /* + "scope_context": { + }, + */ + "strict_evidence_gate": { + "description": "Extract a feature only when the ordinance text explicitly states a requirement, definition, or prohibition for that same feature. Never infer, assume, extrapolate, or guess from related context, implications, headings, or nearby provisions. If the ordinance points to an outside document or standard without restating the controlling requirement in the ordinance text itself, do not import missing values from that outside source." + }, + "data_omission": { + "description": "Emit only positively matched features. If a feature is not explicitly present, omit it entirely rather than returning placeholder text. For qualitative features, use value=null and units=null only when an enacted, explicit requirement or definition for that same feature is present. For numeric features, extract only when an explicit numeric threshold is stated in the ordinance text; otherwise omit the feature instead of returning null, empty, or qualitative-only values. Never emit absence placeholders such as 'not found', 'no explicit requirement', 'none', or similar text in any field." + }, + "numeric_prioritization": { + // Review numeric features if I specify all conditions here. + "description": "When multiple numeric values apply to the same feature, keep one row and select the controlling most restrictive value for that feature. Restrictiveness rules: setbacks -> choose the largest minimum separation distance; tower height -> choose the highest maximum height; noise -> choose the lowest allowed noise limit. Keep condition-specific alternatives in summary only when the ordinance text explicitly shows they all apply to the same feature for GHP systems." + }, + /* + "definition_v_rule_test": { + } + */ + }, "$definitions": { "setback_features": { "description": "Setback features for electrical transmission lines and related infrastructure. Treat each setback feature independently and do not cross-apply a setback unless the ordinance text explicitly states that it applies to multiple target types. When a single clause explicitly lists multiple target types and one shared numeric setback, emit one row per explicitly listed feature using the same numeric value and units and cite the same clause in summary. Apply the shared numeric prioritization rules in $core_principles when multiple numeric values explicitly apply to the same feature.", From 2787e819e2eec49fd08e975753174cea4696e9ff Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Apr 2026 13:10:46 -0600 Subject: [PATCH 015/103] feat: Adding setbacks' properties --- .../transmission/transmission_schema.json5 | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 25cd64e14..236dc4c6d 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -95,9 +95,27 @@ "setback_features": { "description": "Setback features for electrical transmission lines and related infrastructure. Treat each setback feature independently and do not cross-apply a setback unless the ordinance text explicitly states that it applies to multiple target types. When a single clause explicitly lists multiple target types and one shared numeric setback, emit one row per explicitly listed feature using the same numeric value and units and cite the same clause in summary. Apply the shared numeric prioritization rules in $core_principles when multiple numeric values explicitly apply to the same feature.", "properties": { - "property lines distance": { - "description": "Minimum required separation from explicit front yard, side yard, rear yard, or similar yard-based setback areas. Extract this feature only when the ordinance states the electric transmission line and related facilities must meet a yard setback or be located within/outside a named yard area. Do not remap yard setbacks to 'property lines' unless the ordinance explicitly measures the requirement from the property line itself rather than from a yard classification. IGNORE: Do not respond based on generic property-line, road, or right-of-way setbacks unless the ordinance explicitly frames the requirement as a yard setback." + "structures": { + "description": "Minimum required separation from structures and buildings that do not match the definition of 'residential buildings'." }, + "residential buildings": { + "description": "Minimum required separation from residential buildings, occupied dwellings, occupied buildings, residences, homes, residential receptors, or residential uses." + }, + "property lines": { + "description": "Minimum required separation from property lines, lot lines, parcel boundaries, or lease boundaries when the ordinance explicitly states the distance is measured from that boundary. Do not remap property-line distances to roads or residential zones unless the text explicitly makes them equivalent for that requirement. Distances to official plan lines or specific plan lines for public highways do not belong here unless the ordinance expressly defines those lines as property boundaries for the same requirement." + }, + "roads": { + "description": "Minimum required separation from public road rights-of-way. Property-line setbacks do NOT count for this feature unless the ordinance text explicitly states that the property line is the road right-of-way or otherwise makes them the same boundary for that requirement. IGNORE: Do not respond based on setbacks from other kinds of right-of-way such as utility easements, etc." + }, + "airport": { + "description":"Minimum required separation from airport runways and heliports." + }, + "OHWM": { + "description":"Minimum required separation from Ordinary High Water Mark (OHWM)." + }, + "railroads": { + "description": "Minimum required separation from railroads, railroad rights-of-way, rail corridors, or active rail lines. Extract only when rail infrastructure is explicitly named." + } } }, "numerical_features": { From 98f049f8d763d6b4ee109ff207664db54b2227f7 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Apr 2026 17:43:51 -0600 Subject: [PATCH 016/103] This is incomplete, so let's call it 0.0.1 --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 236dc4c6d..97ab1672e 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -1,7 +1,7 @@ { "title": "Transmission Ordinance Extraction Schema", "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all relevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", - "version": "1.0.0", + "version": "0.0.1", "type": "object", "required": ["outputs"], "additionalProperties": false, From 4b88ddd2bb38a0143979eb8f5abf4866de816ab1 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 20 Apr 2026 23:02:46 -0600 Subject: [PATCH 017/103] Finishing operational and including prohibitions --- .../transmission/transmission_schema.json5 | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 97ab1672e..cac429ca2 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -132,8 +132,37 @@ "operational_features": { "description": "Operational, licensing, permitting, inspection, and abandonment requirements. These features require text extraction only.", "properties": { + "bond": { + "description": "Extract requirements for bonds, sureties, or financial assurance instruments related to construction, operation, maintenance, or decommissioning of electric transmission lines and related infrastructure. Include any specific conditions, amounts, or criteria for bond release when explicitly stated." + }, "decommissioning": { - "description": "Extract requirements for abandonment, plugging, sealing, removal, and site restoration when systems are retired, fail, or wells are abandoned, including responsible party and timeline details when explicitly stated." + "description": "Extract requirements for abandonment, removal, and site restoration and when systems are retired, fail, or towers are abandoned, including responsible party and timeline details when explicitly stated." + }, + "permit": { + "description": "Extract requirements for conditional use permits, special use permits, or other discretionary land use approvals that apply to electric transmission lines and related infrastructure. Include any specific conditions or criteria for approval when explicitly stated." + }, + "FAA": { + "description": "Extract requirements for compliance with Federal Aviation Administration (FAA) regulations and local approvals for transmission lines and related infrastructure near airports. Include any specific FAA standards or local approval processes when explicitly stated." + } + } + }, + "prohibition_features": { + "description": "Prohibitions, bans, or moratoria on building, installing, siting, or otherwise deploying electric transmission lines and related infrastructure in certain areas or under certain conditions. These features require text extraction only.", + "properties": { + "rural zone": { + "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in rural zones, agricultural zones, or similar low-density land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + }, + "residential zone": { + "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in residential zones, residential districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + }, + "commercial zone": { + "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in commercial zones, commercial districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + }, + "light industrial zone": { + "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in light industrial zones, light industrial districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + }, + "agricultural zone": { + "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in agricultural zones, agricultural districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure. Include prohibitions in irrigated farmland, ranch land, or similar agricultural land when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure in those areas." } } }, From 9c34758140f8e8399b0386e02a704e04c9296eba Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Apr 2026 14:50:02 -0600 Subject: [PATCH 018/103] feat: match_labels() Pair labels with outputs. --- compass/qc/core.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 compass/qc/core.py diff --git a/compass/qc/core.py b/compass/qc/core.py new file mode 100644 index 000000000..b5b9b86bc --- /dev/null +++ b/compass/qc/core.py @@ -0,0 +1,83 @@ +"""Core functionalities to validate CSV outputs with manual labels""" + +from __future__ import annotations + +import logging +from collections.abc import Generator + +import polars as pl + +from store import location_label + +logger = logging.getLogger(__name__) + + +def match_labels( + truth: dict[str, dict], + lf: pl.LazyFrame, +) -> Generator[tuple[dict, pl.DataFrame], None, None]: + """Pair ground-truth locations with matching run rows + + Iterates over each location in *truth*, builds a + geographic filter (state, county, and subdivision when + defined), applies it to *lf*, and collects the result. + When the truth entry declares a FIPS code, the matched + rows are checked for agreement; mismatches are reported + via ``logging.error``. + + Parameters + ---------- + truth : dict[str, dict] + Ground-truth dict as returned by + ``store.load_truth()``, keyed by normalised + location string. + lf : pl.LazyFrame + Lazy representation of a run CSV, as produced by + ``load_run(path).lazy()``. + + Yields + ------ + tuple[dict, pl.DataFrame] + A pair ``(loc_data, loc_df)`` for each location in + *truth*: + + loc_data + The truth dict for one location, containing + state, county, subdivision, FIPS, and features + with their check specs. + loc_df + Collected DataFrame with every run row that + matches the location geographically. May be + empty when the run has no data for that + location. + """ + for _loc_key, loc_data in truth.items(): + mask = ( + (pl.col("county") == loc_data["county"]) + & (pl.col("state") == loc_data["state"]) + ) + subdiv = loc_data.get("subdivision") + if subdiv: + mask = mask & (pl.col("subdivision") == subdiv) + else: + mask = mask & pl.col("subdivision").is_null() + + loc_df = lf.filter(mask).collect() + + # Validate FIPS agreement + expected_fips = loc_data.get("FIPS") + if expected_fips is not None and not loc_df.is_empty(): + run_fips = loc_df["FIPS"].unique().to_list() + mismatched = [ + f for f in run_fips + if f is not None and f != expected_fips + ] + if mismatched: + loc_lbl = location_label(loc_data) + logger.error( + "FIPS mismatch for %s: truth declares %s, " + "run contains %s", + loc_lbl, expected_fips, mismatched, + ) + + yield loc_data, loc_df From ed5a5f9d31ef1a6794846f0d1320cfd6715f81de Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Apr 2026 14:54:13 -0600 Subject: [PATCH 019/103] feat: Load and parse labeled data --- compass/qc/store.py | 284 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 compass/qc/store.py diff --git a/compass/qc/store.py b/compass/qc/store.py new file mode 100644 index 000000000..f7b31c121 --- /dev/null +++ b/compass/qc/store.py @@ -0,0 +1,284 @@ +""" +truth_store.py — Load, merge, and validate ground-truth YAML files. + +Handles single files, directories (recursive), and duplicate detection +both within and across files. + +Location keys support two granularities: + + County level : "County, State" → e.g. "Power, Idaho" + Township level: "Subdivision, County, State" → e.g. "Springfield, Power, Idaho" + +load_truth() returns a dict grouped by location rather than a flat list, +so downstream code can process and summarise per-location. +""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +# ── Field definitions ──────────────────────────────────────────────────────── +# These define which CSV columns the ground-truth format understands. + +# Columns where exact string match is the natural comparison +EXACT_FIELDS = ["value", "units", "adder", "min_dist", "max_dist", "year"] + +# Columns where text / keyword / not-null matching makes more sense +TEXT_FIELDS = ["summary", "section", "source"] + +ALL_CHECK_FIELDS = EXACT_FIELDS + TEXT_FIELDS + +# ── Exceptions ─────────────────────────────────────────────────────────────── + + +class DuplicateLocationError(Exception): + """Raised when the same location key appears in more than one place.""" + + +# ── Location key parsing ──────────────────────────────────────────────────── + + +def parse_location_key(key: str) -> dict[str, str | None]: + """ + Parse a YAML location key into its component parts. + + Supports: + "County, State" → county-level + "Subdivision, County, State" → township-level + + Returns a dict with keys: state, county, subdivision (None if county-level). + """ + parts = [p.strip() for p in key.split(",")] + + if len(parts) == 2: + return { + "state": parts[1].lower(), + "county": parts[0].lower(), + "subdivision": None, + } + elif len(parts) == 3: + return { + "state": parts[2].lower(), + "county": parts[1].lower(), + "subdivision": parts[0].lower(), + } + else: + raise ValueError( + f"Location key must have 2 parts (County, State) or " + f"3 parts (Subdivision, County, State), got {len(parts)}: '{key}'" + ) + + +def location_label(loc: dict[str, str | None]) -> str: + """ + Build a human-readable label from parsed location components. + + Returns "County, State" or "Subdivision, County, State". + """ + parts = [] + if loc.get("subdivision"): + parts.append(loc["subdivision"].title()) + parts.append(loc["county"].title()) + parts.append(loc["state"].title()) + return ", ".join(parts) + + +# ── File collection ────────────────────────────────────────────────────────── + + +def collect_truth_files(path: str | Path) -> list[Path]: + """ + Walk *path* and return every .yaml / .yml file found. + + If *path* is a single file, return it in a one-element list. + If *path* is a directory, recurse into all sub-folders (sorted for + deterministic ordering). + """ + p = Path(path) + if p.is_file(): + return [p] + if p.is_dir(): + files = sorted(p.rglob("*.yaml")) + sorted(p.rglob("*.yml")) + # rglob may return .yml files that also matched .yaml; deduplicate + seen: set[Path] = set() + unique: list[Path] = [] + for f in files: + resolved = f.resolve() + if resolved not in seen: + seen.add(resolved) + unique.append(f) + return unique + raise FileNotFoundError(f"Ground-truth path not found: {p}") + + +# ── Merging & duplicate detection ──────────────────────────────────────────── + + +def merge_truth_dicts(files: list[Path]) -> dict: + """ + Load every YAML file and merge into one dict. + + Raises DuplicateLocationError if a top-level location key (e.g. + "Power, Idaho") appears more than once — whether across different + files or duplicated inside the same file. + """ + merged: dict = {} + # Track where each key was first seen for the error message + origin: dict[str, Path] = {} + + for fpath in files: + raw = yaml.safe_load(fpath.read_text()) + if raw is None: + continue + if not isinstance(raw, dict): + raise ValueError( + f"Expected a YAML mapping at the top level of {fpath}, " + f"got {type(raw).__name__}" + ) + + # Check for intra-file duplicates. PyYAML silently keeps the last + # occurrence when a key is repeated, so we do a quick text-level + # scan to catch that case before it's swallowed. + _check_intra_file_duplicates(fpath) + + for key in raw: + norm = _normalise_location_key(key) + if norm in origin: + raise DuplicateLocationError( + f"Duplicate location '{key}' — already defined in " + f"{origin[norm]}, found again in {fpath}" + ) + origin[norm] = fpath + merged[key] = raw[key] + + return merged + + +def _normalise_location_key(key: str) -> str: + """Lowercase + strip so 'Power, Idaho' and ' power , idaho ' collide.""" + return ", ".join(p.strip().lower() for p in key.split(",")) + + +def _check_intra_file_duplicates(fpath: Path) -> None: + """ + Detect duplicate top-level keys inside a single YAML file. + + PyYAML's safe_load silently drops all-but-the-last duplicate key, + so we scan the raw text for top-level keys (lines that start at + column 0 and end with ':') and flag repeats. + """ + seen: dict[str, int] = {} + for lineno, line in enumerate(fpath.read_text().splitlines(), start=1): + stripped = line.rstrip() + # Skip blank lines, comments, and indented lines + if not stripped or stripped.startswith("#") or line[0] in (" ", "\t"): + continue + # A top-level key line looks like `"Power, Idaho":` or `Power, Idaho:` + if stripped.endswith(":"): + raw_key = stripped[:-1].strip().strip('"').strip("'") + norm = _normalise_location_key(raw_key) + if norm in seen: + raise DuplicateLocationError( + f"Duplicate location '{raw_key}' inside {fpath} " + f"(lines {seen[norm]} and {lineno})" + ) + seen[norm] = lineno + + +# ── Check-spec builder (internal) ─────────────────────────────────────────── + + +def _build_checks(field_checks: dict) -> dict[str, dict]: + """ + Convert a raw YAML feature block into a checks dict. + + Each key is a field name, each value is a dict describing the match mode. + """ + checks: dict[str, dict] = {} + for fld, spec in field_checks.items(): + if fld not in ALL_CHECK_FIELDS: + continue + if isinstance(spec, dict) and "keywords" in spec: + checks[fld] = { + "mode": "keywords", + "keywords": [str(k).lower() for k in spec["keywords"]], + } + elif spec == "not_null": + checks[fld] = {"mode": "not_null"} + elif spec == "absent": + checks[fld] = {"mode": "absent"} + else: + checks[fld] = {"mode": "exact", "expected": str(spec).strip().lower()} + return checks + + +# ── Main loader ────────────────────────────────────────────────────────────── + + +def load_truth(path: str | Path) -> dict[str, dict]: + """ + Parse ground-truth YAML(s) into a dict grouped by location. + + *path* can be: + - a single .yaml / .yml file + - a directory — every .yaml / .yml underneath is collected and merged + + Raises DuplicateLocationError if any location key appears more than once. + + Returns a dict keyed by normalised location string:: + + { + "power, idaho": { + "state": "idaho", + "county": "power", + "subdivision": None, + "FIPS": "16077", + "features": { + "residential buildings": { + "value": {"mode": "exact", "expected": "1500"}, + "units": {"mode": "exact", "expected": "feet"}, + "summary": {"mode": "keywords", "keywords": [...]}, + ... + }, + "property lines": { ... }, + } + }, + "springfield, power, idaho": { + "state": "idaho", + "county": "power", + "subdivision": "springfield", + ... + }, + } + """ + files = collect_truth_files(path) + if not files: + raise FileNotFoundError(f"No .yaml / .yml files found under {path}") + + raw = merge_truth_dicts(files) + result: dict[str, dict] = {} + + for location_key, loc_data in raw.items(): + loc = parse_location_key(location_key) + norm_key = _normalise_location_key(location_key) + + fips = loc_data.get("FIPS") + raw_features = loc_data.get("features", {}) + + parsed_features: dict[str, dict] = {} + for feat_name, field_checks in raw_features.items(): + if field_checks is None: + continue + checks = _build_checks(field_checks) + if checks: + parsed_features[feat_name.lower()] = checks + + result[norm_key] = { + **loc, + "FIPS": str(fips) if fips is not None else None, + "features": parsed_features, + } + + return result From 412b8b31a65fcb490271cf526b6378a983c533c0 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 18 May 2026 16:13:32 -0600 Subject: [PATCH 020/103] feat: find_missing_features() --- compass/qc/core.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/compass/qc/core.py b/compass/qc/core.py index b5b9b86bc..fd611cc31 100644 --- a/compass/qc/core.py +++ b/compass/qc/core.py @@ -81,3 +81,56 @@ def match_labels( ) yield loc_data, loc_df + + +def find_missing_features( + loc_data: dict, + lf: pl.LazyFrame, +) -> list[str]: + """ + Find features declared in truth but absent from the run. + + Compares the feature names listed in *loc_data* against + the distinct ``feature`` values present in *lf*. Any + feature that appears in the truth but has no matching + row in the run is considered missing. + + Designed to compose with :func:`match_labels`:: + + for loc_data, loc_df in match_labels(truth, run_lf): + missing = find_missing_features( + loc_data, loc_df.lazy(), + ) + + Parameters + ---------- + loc_data : dict + Truth dict for a single location, as yielded by + :func:`match_labels`. Must contain a ``features`` + key mapping feature names to check specs (which + may be empty dicts for presence-only features). + lf : pl.LazyFrame + Lazy representation of the run rows already scoped + to this location. + + Returns + ------- + list[str] + Feature names present in the truth but not found + in the run, in the order they appear in + ``loc_data["features"]``. Empty list when all + features are present. + """ + expected = set(loc_data.get("features", {}).keys()) + if not expected: + return [] + + present = set( + lf.select("feature") + .unique() + .collect() + .get_column("feature") + .to_list() + ) + + return [f for f in loc_data["features"] if f not in present] From 69b1f9fa42e736c52fb74932e7db9e06b7109f94 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 07:14:11 -0600 Subject: [PATCH 021/103] refact: Renaming store.py to reference.py --- compass/qc/{store.py => reference.py} | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) rename compass/qc/{store.py => reference.py} (92%) diff --git a/compass/qc/store.py b/compass/qc/reference.py similarity index 92% rename from compass/qc/store.py rename to compass/qc/reference.py index f7b31c121..dc6f0a11b 100644 --- a/compass/qc/store.py +++ b/compass/qc/reference.py @@ -1,5 +1,5 @@ """ -truth_store.py — Load, merge, and validate ground-truth YAML files. +reference.py — Load, merge, and validate reference YAML files. Handles single files, directories (recursive), and duplicate detection both within and across files. @@ -9,7 +9,7 @@ County level : "County, State" → e.g. "Power, Idaho" Township level: "Subdivision, County, State" → e.g. "Springfield, Power, Idaho" -load_truth() returns a dict grouped by location rather than a flat list, +load_reference() returns a dict grouped by location rather than a flat list, so downstream code can process and summarise per-location. """ @@ -20,7 +20,7 @@ import yaml # ── Field definitions ──────────────────────────────────────────────────────── -# These define which CSV columns the ground-truth format understands. +# These define which CSV columns the reference format understands. # Columns where exact string match is the natural comparison EXACT_FIELDS = ["value", "units", "adder", "min_dist", "max_dist", "year"] @@ -88,7 +88,7 @@ def location_label(loc: dict[str, str | None]) -> str: # ── File collection ────────────────────────────────────────────────────────── -def collect_truth_files(path: str | Path) -> list[Path]: +def collect_reference_files(path: str | Path) -> list[Path]: """ Walk *path* and return every .yaml / .yml file found. @@ -110,13 +110,13 @@ def collect_truth_files(path: str | Path) -> list[Path]: seen.add(resolved) unique.append(f) return unique - raise FileNotFoundError(f"Ground-truth path not found: {p}") + raise FileNotFoundError(f"Reference path not found: {p}") # ── Merging & duplicate detection ──────────────────────────────────────────── -def merge_truth_dicts(files: list[Path]) -> dict: +def merge_reference_dicts(files: list[Path]) -> dict: """ Load every YAML file and merge into one dict. @@ -217,9 +217,9 @@ def _build_checks(field_checks: dict) -> dict[str, dict]: # ── Main loader ────────────────────────────────────────────────────────────── -def load_truth(path: str | Path) -> dict[str, dict]: +def load_reference(path: str | Path) -> dict[str, dict]: """ - Parse ground-truth YAML(s) into a dict grouped by location. + Parse reference YAML(s) into a dict grouped by location. *path* can be: - a single .yaml / .yml file @@ -253,11 +253,11 @@ def load_truth(path: str | Path) -> dict[str, dict]: }, } """ - files = collect_truth_files(path) + files = collect_reference_files(path) if not files: raise FileNotFoundError(f"No .yaml / .yml files found under {path}") - raw = merge_truth_dicts(files) + raw = merge_reference_dicts(files) result: dict[str, dict] = {} for location_key, loc_data in raw.items(): @@ -270,10 +270,12 @@ def load_truth(path: str | Path) -> dict[str, dict]: parsed_features: dict[str, dict] = {} for feat_name, field_checks in raw_features.items(): if field_checks is None: + # Feature listed with no arguments — still + # track it so presence checks can catch it. + parsed_features[feat_name.lower()] = {} continue checks = _build_checks(field_checks) - if checks: - parsed_features[feat_name.lower()] = checks + parsed_features[feat_name.lower()] = checks result[norm_key] = { **loc, From 469335ef49b08c84c7ef1c146889fbfb6d58a7a5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 09:00:12 -0600 Subject: [PATCH 022/103] refact: Using 'reference' instead of 'truth' --- compass/qc/core.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/compass/qc/core.py b/compass/qc/core.py index fd611cc31..5b24ebee7 100644 --- a/compass/qc/core.py +++ b/compass/qc/core.py @@ -7,29 +7,29 @@ import polars as pl -from store import location_label +from reference import location_label logger = logging.getLogger(__name__) def match_labels( - truth: dict[str, dict], + ref: dict[str, dict], lf: pl.LazyFrame, ) -> Generator[tuple[dict, pl.DataFrame], None, None]: """Pair ground-truth locations with matching run rows - Iterates over each location in *truth*, builds a + Iterates over each location in *ref*, builds a geographic filter (state, county, and subdivision when defined), applies it to *lf*, and collects the result. - When the truth entry declares a FIPS code, the matched + When the reference entry declares a FIPS code, the matched rows are checked for agreement; mismatches are reported via ``logging.error``. Parameters ---------- - truth : dict[str, dict] - Ground-truth dict as returned by - ``store.load_truth()``, keyed by normalised + ref : dict[str, dict] + Reference dict as returned by + ``reference.load_reference()``, keyed by normalised location string. lf : pl.LazyFrame Lazy representation of a run CSV, as produced by @@ -39,10 +39,10 @@ def match_labels( ------ tuple[dict, pl.DataFrame] A pair ``(loc_data, loc_df)`` for each location in - *truth*: + *ref*: loc_data - The truth dict for one location, containing + The reference dict for one location, containing state, county, subdivision, FIPS, and features with their check specs. loc_df @@ -51,7 +51,7 @@ def match_labels( empty when the run has no data for that location. """ - for _loc_key, loc_data in truth.items(): + for _loc_key, loc_data in ref.items(): mask = ( (pl.col("county") == loc_data["county"]) & (pl.col("state") == loc_data["state"]) @@ -75,7 +75,7 @@ def match_labels( if mismatched: loc_lbl = location_label(loc_data) logger.error( - "FIPS mismatch for %s: truth declares %s, " + "FIPS mismatch for %s: reference declares %s, " "run contains %s", loc_lbl, expected_fips, mismatched, ) @@ -88,16 +88,16 @@ def find_missing_features( lf: pl.LazyFrame, ) -> list[str]: """ - Find features declared in truth but absent from the run. + Find features declared in the reference but absent from the run. Compares the feature names listed in *loc_data* against the distinct ``feature`` values present in *lf*. Any - feature that appears in the truth but has no matching + feature that appears in the reference but has no matching row in the run is considered missing. Designed to compose with :func:`match_labels`:: - for loc_data, loc_df in match_labels(truth, run_lf): + for loc_data, loc_df in match_labels(ref, run_lf): missing = find_missing_features( loc_data, loc_df.lazy(), ) @@ -116,7 +116,7 @@ def find_missing_features( Returns ------- list[str] - Feature names present in the truth but not found + Feature names present in the reference but not found in the run, in the order they appear in ``loc_data["features"]``. Empty list when all features are present. From 632589db0b382507b6df99ac4e0f282e2bec7ee5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 10:00:34 -0600 Subject: [PATCH 023/103] feat: extract_locations + find_missing_locations --- compass/qc/core.py | 95 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/compass/qc/core.py b/compass/qc/core.py index 5b24ebee7..ee9e22dc5 100644 --- a/compass/qc/core.py +++ b/compass/qc/core.py @@ -1,9 +1,12 @@ -"""Core functionalities to validate CSV outputs with manual labels""" +""" +core.py — Composable building blocks for matching and validation +""" from __future__ import annotations import logging from collections.abc import Generator +from dataclasses import dataclass import polars as pl @@ -12,6 +15,96 @@ logger = logging.getLogger(__name__) +# ── Data types ─────────────────────────────────────────────────────── + + +@dataclass +class CheckResult: + """Outcome of a single field-level check""" + + field: str + mode: str + passed: bool + expected: str + actual: str + detail: str = "" + + +# ── Location-level functions ───────────────────────────────────────── + + +def extract_locations( + lf: pl.LazyFrame, +) -> set[tuple[str, str, str | None]]: + """ + Collect distinct locations from a LazyFrame + + Each location is a tuple of ``(state, county, subdivision)`` + where *subdivision* is ``None`` for county-level records. + + Parameters + ---------- + lf : pl.LazyFrame + Lazy representation of run data containing at least + the columns ``state``, ``county``, and + ``subdivision``. + + Returns + ------- + set[tuple[str, str, str | None]] + Unique location tuples found in the data. + """ + rows = ( + lf.select("state", "county", "subdivision") + .unique() + .collect() + .iter_rows() + ) + return {(state, county, subdiv) for state, county, subdiv in rows} + + +def find_missing_locations( + ref: dict[str, dict], + lf: pl.LazyFrame, +) -> list[dict]: + """ + Find reference locations absent from the target + + Compares the locations declared in *ref* against the + distinct locations present in *lf*. Returns the + reference entries whose geographic key (state, county, + subdivision) has no matching rows in the target. + + Parameters + ---------- + ref : dict[str, dict] + Reference dict as returned by + ``reference.load_reference()``. + lf : pl.LazyFrame + Lazy representation of the target run data. + + Returns + ------- + list[dict] + The ``loc_data`` dicts for each reference location + not found in the target, in the order they appear + in *ref*. Empty list when all reference locations + are present. + """ + target_locs = extract_locations(lf) + + missing = [] + for _loc_key, loc_data in ref.items(): + loc_tuple = ( + loc_data["state"], + loc_data["county"], + loc_data.get("subdivision"), + ) + if loc_tuple not in target_locs: + missing.append(loc_data) + return missing + + def match_labels( ref: dict[str, dict], lf: pl.LazyFrame, From 737503c8816fc08781574c8a542793f22db87794 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 11:39:39 -0600 Subject: [PATCH 024/103] wip: CLI definition --- compass/qc/schema_eval.py | 513 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 compass/qc/schema_eval.py diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py new file mode 100644 index 000000000..94b847701 --- /dev/null +++ b/compass/qc/schema_eval.py @@ -0,0 +1,513 @@ +""" +schema_eval.py — CLI entry point for extraction evaluation + +Subcommands +----------- + init Scaffold a reference YAML from an existing CSV run. + validate Score one CSV run against reference. + compare Diff two CSV runs; optionally score both against reference. + +Examples +-------- + python schema_eval.py init run1.csv -o reference.yaml + python schema_eval.py validate run1.csv -t reference.yaml + python schema_eval.py compare run1.csv run2.csv -t reference.yaml +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +import polars as pl + +from core import ( + CheckResult, + extract_locations, + find_feature_row, + find_missing_features, + find_missing_locations, + match_labels, + run_checks, + score_run, + validate_formated, +) +from reference import ( + ALL_CHECK_FIELDS, + EXACT_FIELDS, + TEXT_FIELDS, + load_reference, + location_label, +) + +# ── Constants ──────────────────────────────────────────────────────── + +KEY_COLS = ["county", "state", "subdivision", "feature"] + +# ── ANSI helpers ───────────────────────────────────────────────────── + + +class C: + """Tiny ANSI colour helpers""" + + GREEN = "\033[92m" + RED = "\033[91m" + YELLOW = "\033[93m" + CYAN = "\033[96m" + BOLD = "\033[1m" + DIM = "\033[2m" + RESET = "\033[0m" + + @staticmethod + def ok(s: str) -> str: return f"{C.GREEN}{s}{C.RESET}" + @staticmethod + def fail(s: str) -> str: return f"{C.RED}{s}{C.RESET}" + @staticmethod + def warn(s: str) -> str: return f"{C.YELLOW}{s}{C.RESET}" + @staticmethod + def bold(s: str) -> str: return f"{C.BOLD}{s}{C.RESET}" + + +# ── Data loading ───────────────────────────────────────────────────── + + +def load_run(path: str | Path) -> pl.DataFrame: + """Read a CSV run, normalise key columns to stripped lowercase""" + df = pl.read_csv(path, infer_schema_length=0) + if "subdivision" not in df.columns: + df = df.with_columns( + pl.lit(None).cast(pl.Utf8).alias("subdivision") + ) + if "fips" in df.columns and "FIPS" not in df.columns: + df = df.rename({"fips": "FIPS"}) + df = df.with_columns( + pl.col(c).str.strip_chars().str.to_lowercase() + for c in KEY_COLS if c in df.columns + ) + for c in df.columns: + df = df.with_columns( + pl.when(pl.col(c).str.strip_chars() == "") + .then(None) + .otherwise(pl.col(c).str.strip_chars()) + .alias(c) + ) + return df + + +# ── Formatting helpers ─────────────────────────────────────────────── + + +def _truncate(s: str | None, n: int) -> str: + """Shorten a string for display""" + if s is None: + return "(null)" + return s[:n] + "…" if len(s) > n else s + + +def _pct_color(pct: float) -> callable: + """Pick a colour function based on percentage thresholds""" + if pct >= 90: + return C.ok + return C.warn if pct >= 70 else C.fail + + +def _sortable_key(t: tuple) -> tuple: + """Replace None with '' so tuples are sortable""" + return tuple(v if v is not None else "" for v in t) + + +# ── Validate subcommand ────────────────────────────────────────────── + + +def cmd_validate( + run_path: str, + ref_path: str, + verbose: bool = False, + output_format: str = "text", +): + df = load_run(run_path) + ref = load_reference(ref_path) + + return validate_formated( + ref, + df.lazy(), + run_path, + ref_path, + output_format=output_format, + verbose=verbose, + style=C, + ) + + +# ── Compare subcommand ─────────────────────────────────────────────── + + +def cmd_compare( + run_a_path: str, + run_b_path: str, + ref_path: str | None = None, + verbose: bool = False, +): + df_a = load_run(run_a_path) + df_b = load_run(run_b_path) + + label_a = Path(run_a_path).stem + label_b = Path(run_b_path).stem + + print(C.bold(f"\n{'='*70}")) + print(C.bold(f" Comparison: {label_a} vs {label_b}")) + print(C.bold(f"{'='*70}\n")) + + # Build key sets — tuples of (county, state, subdivision, feature) + def key_set(df: pl.DataFrame) -> set[tuple]: + return set(df.select(KEY_COLS).unique().iter_rows()) + + keys_a = key_set(df_a) + keys_b = key_set(df_b) + + only_a = keys_a - keys_b + only_b = keys_b - keys_a + common = keys_a & keys_b + + # ── Row-presence diff ──────────────────────────────────────── + if only_a or only_b: + print(C.bold(" Row presence changes:")) + if only_a: + print( + f"\n {C.fail(f'Removed in {label_b}')}" + f" ({len(only_a)}):" + ) + for c, s, sd, f in sorted(only_a, key=_sortable_key): + loc = location_label( + {"county": c, "state": s, "subdivision": sd} + ) + print(f" − {loc} → {f}") + if only_b: + print( + f"\n {C.ok(f'Added in {label_b}')}" + f" ({len(only_b)}):" + ) + for c, s, sd, f in sorted(only_b, key=_sortable_key): + loc = location_label( + {"county": c, "state": s, "subdivision": sd} + ) + print(f" + {loc} → {f}") + print() + + # ── Field-level diff on shared rows ────────────────────────── + compare_fields = [ + f for f in ALL_CHECK_FIELDS + if f in df_a.columns and f in df_b.columns + ] + n_changed = 0 + n_unchanged = 0 + + for key in sorted(common, key=_sortable_key): + county, state, subdiv, feature = key + + def _filter(df, c, s, sd, f): + mask = ( + (pl.col("county") == c) + & (pl.col("state") == s) + & (pl.col("feature") == f) + ) + if sd: + mask &= pl.col("subdivision") == sd + else: + mask &= pl.col("subdivision").is_null() + return df.filter(mask) + + row_a = _filter( + df_a, county, state, subdiv, feature + ).row(0, named=True) + row_b = _filter( + df_b, county, state, subdiv, feature + ).row(0, named=True) + + diffs: list[tuple[str, str | None, str | None]] = [] + for fld in compare_fields: + va = row_a.get(fld) + vb = row_b.get(fld) + na = va.strip().lower() if va else None + nb = vb.strip().lower() if vb else None + if na != nb: + diffs.append((fld, va, vb)) + + loc = location_label( + {"county": county, "state": state, "subdivision": subdiv} + ) + label = f"{loc} → {feature}" + + if diffs: + n_changed += 1 + print(f" {C.warn('CHANGED')} {label}") + for fld, va, vb in diffs: + va_d = _truncate(va, 40) if va else "(null)" + vb_d = _truncate(vb, 40) if vb else "(null)" + print( + f" {C.DIM}├─{C.RESET} {fld}:" + f" {C.fail(va_d)} → {C.ok(vb_d)}" + ) + elif verbose: + n_unchanged += 1 + print(f" {C.DIM}SAME{C.RESET} {label}") + else: + n_unchanged += 1 + + # ── Comparison summary ─────────────────────────────────────── + print(C.bold(f"\n{'─'*70}")) + print(C.bold(" Comparison summary")) + print(f"{'─'*70}") + print(f" Rows only in {label_a}: {len(only_a)}") + print(f" Rows only in {label_b}: {len(only_b)}") + print(f" Shared rows, changed : {C.warn(str(n_changed))}") + print(f" Shared rows, same : {n_unchanged}") + print() + + # ── Optional: score both against reference ─────────────────── + if ref_path: + _print_ref_scoring( + ref_path, df_a, df_b, label_a, label_b, + ) + + +def _print_ref_scoring(ref_path, df_a, df_b, label_a, label_b): + """Score both runs against reference and show divergences""" + ref = load_reference(ref_path) + + print(C.bold(f"{'─'*70}")) + print(C.bold(" Reference scoring")) + print(f"{'─'*70}\n") + + for label, df in [(label_a, df_a), (label_b, df_b)]: + passed, total = score_run(ref, df.lazy()) + pct = (passed / total * 100) if total else 0 + clr = _pct_color(pct) + print(f" {label:.<40s} {clr(f'{passed}/{total}')} ({pct:.1f}%)") + + print() + + divergences = _find_divergences(ref, df_a, df_b) + if divergences: + print(f" {C.bold('Divergent reference results')}:\n") + for d in divergences: + print(f" {d['location']} · {d['field']}") + sa = C.ok("✓") if d["a_pass"] else C.fail("✗") + sb = C.ok("✓") if d["b_pass"] else C.fail("✗") + print( + f" {label_a}: {sa} {label_b}: {sb}" + f" — {d['detail']}" + ) + print() + + +def _find_divergences( + ref: dict[str, dict], + df_a: pl.DataFrame, + df_b: pl.DataFrame, +) -> list[dict]: + """Find checks where two runs disagree against the reference""" + slices_a = { + location_label(loc_data): loc_df + for loc_data, loc_df in match_labels(ref, df_a.lazy()) + } + + divs = [] + for loc_data, loc_df_b in match_labels(ref, df_b.lazy()): + loc_lbl = location_label(loc_data) + loc_df_a = slices_a.get(loc_lbl, pl.DataFrame()) + + for feat_name, checks in loc_data["features"].items(): + feat_label = f"{loc_lbl} → {feat_name}" + row_a = find_feature_row(loc_df_a, feat_name) + row_b = find_feature_row(loc_df_b, feat_name) + + for fld, check in checks.items(): + res_a = ( + run_checks(row_a, {fld: check}) if row_a + else [CheckResult( + fld, check["mode"], False, + "", "(missing)", "row missing", + )] + ) + res_b = ( + run_checks(row_b, {fld: check}) if row_b + else [CheckResult( + fld, check["mode"], False, + "", "(missing)", "row missing", + )] + ) + if res_a[0].passed != res_b[0].passed: + divs.append({ + "location": feat_label, + "field": fld, + "a_pass": res_a[0].passed, + "b_pass": res_b[0].passed, + "detail": ( + f"A: {res_a[0].actual[:50]}" + f" B: {res_b[0].actual[:50]}" + ), + }) + return divs + + +# ── Init subcommand ────────────────────────────────────────────────── + + +def cmd_init(run_path: str, output_path: str): + """Generate a reference YAML template from an existing CSV run""" + df = load_run(run_path) + + grouped: dict[str, dict[str, Any]] = {} + for row in df.iter_rows(named=True): + county = row["county"] or "unknown" + state = row["state"] or "unknown" + subdiv = row.get("subdivision") + feature = row["feature"] or "unknown" + fips = row.get("FIPS", "") + + loc_key = location_label({ + "county": county, "state": state, + "subdivision": subdiv, + }) + + if loc_key not in grouped: + grouped[loc_key] = {"FIPS": fips, "features": {}} + + feat_entry: dict[str, Any] = {} + for fld in EXACT_FIELDS: + v = row.get(fld) + if v: + feat_entry[fld] = v + for fld in TEXT_FIELDS: + v = row.get(fld) + if v: + feat_entry[fld] = "not_null" + + grouped[loc_key]["features"][feature] = ( + feat_entry if feat_entry else None + ) + + out = Path(output_path) + lines = [ + "# Reference template — generated from: " + + Path(run_path).name, + "# Review each entry and adjust match modes:", + '# exact value → value: "1500"', + "# keywords → summary:", + "# keywords: [word1, word2]", + "# not_null → section: not_null", + "# absent → adder: absent", + "# remove line → field won't be checked", + "#", + "# Location keys:", + '# County level → "County, State"', + '# Township level → "Subdivision, County, State"', + "", + ] + + for loc_key in sorted(grouped): + data = grouped[loc_key] + lines.append(f'"{loc_key}":') + if data["FIPS"]: + lines.append(f' FIPS: "{data["FIPS"]}"') + lines.append(" features:") + for feat_name in sorted(data["features"]): + lines.append("") + lines.append(f" {feat_name}:") + feat = data["features"][feat_name] + if feat is None: + lines.append(" # (no fields extracted)") + continue + for fld, val in feat.items(): + if val == "not_null": + lines.append(f" {fld}: not_null") + else: + lines.append(f' {fld}: "{val}"') + lines.append("") + + out.write_text("\n".join(lines)) + print(f"\n {C.ok('✓')} Template written to {C.bold(str(out))}") + print( + f" {C.DIM}Edit the file to set expected values" + f" and match modes.{C.RESET}\n" + ) + + +# ── CLI ────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Evaluate and compare LLM extraction runs " + "against reference." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + sub = parser.add_subparsers(dest="command", required=True) + + p_init = sub.add_parser( + "init", + help="Scaffold reference YAML from a CSV run", + ) + p_init.add_argument("run", help="Path to the CSV run file") + p_init.add_argument( + "-o", "--output", + default="ground_truth.yaml", help="Output YAML path", + ) + + p_val = sub.add_parser( + "validate", + help="Validate a CSV run against reference", + ) + p_val.add_argument("run", help="Path to the CSV run file") + p_val.add_argument( + "-t", "--ref", required=True, + help="Path to reference YAML file or directory", + ) + p_val.add_argument( + "-v", "--verbose", action="store_true", + help="Show passing checks too", + ) + p_val.add_argument( + "-f", "--format", + choices=["text", "json"], + default="text", + help="Output format for validation report", + ) + + p_cmp = sub.add_parser( + "compare", help="Compare two CSV runs", + ) + p_cmp.add_argument( + "run_a", help="Path to the first (baseline) CSV run", + ) + p_cmp.add_argument( + "run_b", help="Path to the second (new) CSV run", + ) + p_cmp.add_argument( + "-t", "--ref", default=None, + help="Optional reference YAML file or directory", + ) + p_cmp.add_argument( + "-v", "--verbose", action="store_true", + help="Show unchanged rows too", + ) + + args = parser.parse_args() + + if args.command == "init": + cmd_init(args.run, args.output) + elif args.command == "validate": + print(cmd_validate( + args.run, args.ref, args.verbose, args.format + )) + elif args.command == "compare": + cmd_compare(args.run_a, args.run_b, args.ref, args.verbose) + + +if __name__ == "__main__": + main() From 6fd7efd776c0dd5aecc812e6f70bef46b1a590c9 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 11:48:37 -0600 Subject: [PATCH 025/103] refact: Moving load_run to core.py --- compass/qc/schema_eval.py | 56 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py index 94b847701..1c0f4d9f2 100644 --- a/compass/qc/schema_eval.py +++ b/compass/qc/schema_eval.py @@ -28,6 +28,7 @@ find_feature_row, find_missing_features, find_missing_locations, + load_run, match_labels, run_checks, score_run, @@ -69,32 +70,6 @@ def warn(s: str) -> str: return f"{C.YELLOW}{s}{C.RESET}" def bold(s: str) -> str: return f"{C.BOLD}{s}{C.RESET}" -# ── Data loading ───────────────────────────────────────────────────── - - -def load_run(path: str | Path) -> pl.DataFrame: - """Read a CSV run, normalise key columns to stripped lowercase""" - df = pl.read_csv(path, infer_schema_length=0) - if "subdivision" not in df.columns: - df = df.with_columns( - pl.lit(None).cast(pl.Utf8).alias("subdivision") - ) - if "fips" in df.columns and "FIPS" not in df.columns: - df = df.rename({"fips": "FIPS"}) - df = df.with_columns( - pl.col(c).str.strip_chars().str.to_lowercase() - for c in KEY_COLS if c in df.columns - ) - for c in df.columns: - df = df.with_columns( - pl.when(pl.col(c).str.strip_chars() == "") - .then(None) - .otherwise(pl.col(c).str.strip_chars()) - .alias(c) - ) - return df - - # ── Formatting helpers ─────────────────────────────────────────────── @@ -126,12 +101,31 @@ def cmd_validate( verbose: bool = False, output_format: str = "text", ): - df = load_run(run_path) + """Validate a run against reference and return formatted output + + Parameters + ---------- + run_path : str + Path to the CSV run file to validate. + ref_path : str + Path to the reference YAML file or directory. + verbose : bool, default=False + Include passing checks in text output. By default, False. + output_format : str, default="text" + Output format to render. Supported values are ``"text"`` + and ``"json"``. By default, text. + + Returns + ------- + str + Rendered validation report as text or JSON string. + """ + lf = load_run(run_path) ref = load_reference(ref_path) return validate_formated( ref, - df.lazy(), + lf, run_path, ref_path, output_format=output_format, @@ -149,8 +143,8 @@ def cmd_compare( ref_path: str | None = None, verbose: bool = False, ): - df_a = load_run(run_a_path) - df_b = load_run(run_b_path) + df_a = load_run(run_a_path).collect() + df_b = load_run(run_b_path).collect() label_a = Path(run_a_path).stem label_b = Path(run_b_path).stem @@ -357,7 +351,7 @@ def _find_divergences( def cmd_init(run_path: str, output_path: str): """Generate a reference YAML template from an existing CSV run""" - df = load_run(run_path) + df = load_run(run_path).collect() grouped: dict[str, dict[str, Any]] = {} for row in df.iter_rows(named=True): From 4fcf2f977f2d9919d85393d8552e61e8b88fc96b Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 19 May 2026 14:49:17 -0600 Subject: [PATCH 026/103] refact: Moving to Click instead of Argparse --- compass/qc/schema_eval.py | 152 +++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 69 deletions(-) diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py index 1c0f4d9f2..3ee7549c7 100644 --- a/compass/qc/schema_eval.py +++ b/compass/qc/schema_eval.py @@ -16,10 +16,10 @@ from __future__ import annotations -import argparse from pathlib import Path from typing import Any +import click import polars as pl from core import ( @@ -432,75 +432,89 @@ def cmd_init(run_path: str, output_path: str): # ── CLI ────────────────────────────────────────────────────────────── +@click.group( + epilog=__doc__, + context_settings={"help_option_names": ["-h", "--help"]}, +) def main(): - parser = argparse.ArgumentParser( - description=( - "Evaluate and compare LLM extraction runs " - "against reference." - ), - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__, - ) - sub = parser.add_subparsers(dest="command", required=True) - - p_init = sub.add_parser( - "init", - help="Scaffold reference YAML from a CSV run", - ) - p_init.add_argument("run", help="Path to the CSV run file") - p_init.add_argument( - "-o", "--output", - default="ground_truth.yaml", help="Output YAML path", - ) - - p_val = sub.add_parser( - "validate", - help="Validate a CSV run against reference", - ) - p_val.add_argument("run", help="Path to the CSV run file") - p_val.add_argument( - "-t", "--ref", required=True, - help="Path to reference YAML file or directory", - ) - p_val.add_argument( - "-v", "--verbose", action="store_true", - help="Show passing checks too", - ) - p_val.add_argument( - "-f", "--format", - choices=["text", "json"], - default="text", - help="Output format for validation report", - ) - - p_cmp = sub.add_parser( - "compare", help="Compare two CSV runs", - ) - p_cmp.add_argument( - "run_a", help="Path to the first (baseline) CSV run", - ) - p_cmp.add_argument( - "run_b", help="Path to the second (new) CSV run", - ) - p_cmp.add_argument( - "-t", "--ref", default=None, - help="Optional reference YAML file or directory", - ) - p_cmp.add_argument( - "-v", "--verbose", action="store_true", - help="Show unchanged rows too", - ) - - args = parser.parse_args() - - if args.command == "init": - cmd_init(args.run, args.output) - elif args.command == "validate": - print(cmd_validate( - args.run, args.ref, args.verbose, args.format - )) - elif args.command == "compare": - cmd_compare(args.run_a, args.run_b, args.ref, args.verbose) + """Evaluate and compare LLM extraction runs against reference""" + + +@main.command("init") +@click.argument("run") +@click.option( + "-o", + "--output", + "output_path", + default="ground_truth.yaml", + show_default=True, + help="Output YAML path", +) +def init_command(run: str, output_path: str): + """Scaffold reference YAML from a CSV run""" + cmd_init(run, output_path) + + +@main.command("validate") +@click.argument("run") +@click.option( + "-t", + "--ref", + "ref_path", + required=True, + help="Path to reference YAML file or directory", +) +@click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + help="Show passing checks too", +) +@click.option( + "-f", + "--format", + "output_format", + type=click.Choice(["text", "json"]), + default="text", + show_default=True, + help="Output format for validation report", +) +def validate_command( + run: str, + ref_path: str, + verbose: bool, + output_format: str, +): + """Validate a CSV run against reference""" + print(cmd_validate(run, ref_path, verbose, output_format)) + + +@main.command("compare") +@click.argument("run_a") +@click.argument("run_b") +@click.option( + "-t", + "--ref", + "ref_path", + default=None, + help="Optional reference YAML file or directory", +) +@click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + help="Show unchanged rows too", +) +def compare_command( + run_a: str, + run_b: str, + ref_path: str | None, + verbose: bool, +): + """Compare two CSV runs""" + cmd_compare(run_a, run_b, ref_path, verbose) if __name__ == "__main__": From 0a22ccdf6d41202b30ad1d01b88ec060d991d368 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 25 May 2026 19:09:17 -0600 Subject: [PATCH 027/103] Initializing transmission plugin config --- .../transmission/transmission_plugin_config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 compass/extraction/transmission/transmission_plugin_config.yaml diff --git a/compass/extraction/transmission/transmission_plugin_config.yaml b/compass/extraction/transmission/transmission_plugin_config.yaml new file mode 100644 index 000000000..8e272db22 --- /dev/null +++ b/compass/extraction/transmission/transmission_plugin_config.yaml @@ -0,0 +1,15 @@ +schema: ./transmission_schema.json5 + +data_type_short_desc: high-voltage transmission lines ordinance + +query_templates: + - "filetype:pdf {jurisdiction} electric transmission line ordinance" + +website_keywords: + pdf: 92160 + +heuristic_keywords: true + +collection_prompts: true + +cache_llm_generated_content: true From 5dd91aecb09000d48f5b8a6826ce83518ce61471 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 25 May 2026 20:41:48 -0600 Subject: [PATCH 028/103] Updating query_templates --- .../extraction/transmission/transmission_plugin_config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compass/extraction/transmission/transmission_plugin_config.yaml b/compass/extraction/transmission/transmission_plugin_config.yaml index 8e272db22..d15281303 100644 --- a/compass/extraction/transmission/transmission_plugin_config.yaml +++ b/compass/extraction/transmission/transmission_plugin_config.yaml @@ -4,6 +4,10 @@ data_type_short_desc: high-voltage transmission lines ordinance query_templates: - "filetype:pdf {jurisdiction} electric transmission line ordinance" + - "{jurisdiction} high voltage transmission line zoning ordinance" + - "{jurisdiction} electric transmission line siting code" + - "{jurisdiction} overhead electric utility line ordinance" + - "{jurisdiction} transmission line right-of-way regulation" website_keywords: pdf: 92160 From 9bafa01197c3db996afe1c213ce12e52cc3ec853 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 25 May 2026 22:13:39 -0600 Subject: [PATCH 029/103] Updating website_keyword --- .../transmission_plugin_config.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/compass/extraction/transmission/transmission_plugin_config.yaml b/compass/extraction/transmission/transmission_plugin_config.yaml index d15281303..341da9fa4 100644 --- a/compass/extraction/transmission/transmission_plugin_config.yaml +++ b/compass/extraction/transmission/transmission_plugin_config.yaml @@ -11,6 +11,25 @@ query_templates: website_keywords: pdf: 92160 + transmission: 46080 + ordinance: 23040 + zoning: 11520 + utility: 5760 + electric: 5760 + right-of-way: 1440 + overhead: 720 + permit: 720 + land use: 720 + municipal: 360 + county: 360 + code of ordinances: 360 + ordinance code: 360 + land use code: 360 + conditional use permit: 180 + special use permit: 180 + statute: 180 + administrative code: 180 + government: 180 heuristic_keywords: true From fbf158ef58dc9bddb7bb98e559eea6bde7f5409b Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 25 May 2026 22:30:38 -0600 Subject: [PATCH 030/103] refact: Moving back to plugin_config.yaml --- .../{transmission_plugin_config.yaml => plugin_config.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename compass/extraction/transmission/{transmission_plugin_config.yaml => plugin_config.yaml} (100%) diff --git a/compass/extraction/transmission/transmission_plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml similarity index 100% rename from compass/extraction/transmission/transmission_plugin_config.yaml rename to compass/extraction/transmission/plugin_config.yaml From 1162995b28032a13817f17694cfacd7e08572f4c Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 3 Jun 2026 08:09:11 -0600 Subject: [PATCH 031/103] Registering transmission plugin --- compass/extraction/__init__.py | 1 + compass/extraction/transmission/__init__.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 compass/extraction/transmission/__init__.py diff --git a/compass/extraction/__init__.py b/compass/extraction/__init__.py index e86d8f6d7..427a37fdf 100644 --- a/compass/extraction/__init__.py +++ b/compass/extraction/__init__.py @@ -15,5 +15,6 @@ from .rmp import COMPASSGeoRMPExtractor from .small_wind import COMPASSSmallWindExtractor from .solar import COMPASSSolarExtractor +from .transmission import COMPASSTransmissionExtractor from .water import TexasWaterRightsExtractor from .wind import COMPASSWindExtractor diff --git a/compass/extraction/transmission/__init__.py b/compass/extraction/transmission/__init__.py new file mode 100644 index 000000000..e0c458fc0 --- /dev/null +++ b/compass/extraction/transmission/__init__.py @@ -0,0 +1,12 @@ +"""COMPASS transmission extraction plugin""" + +import importlib.resources + +from compass.plugin import create_schema_based_one_shot_extraction_plugin + + +COMPASSTransmissionExtractor = create_schema_based_one_shot_extraction_plugin( + importlib.resources.files("compass.extraction.transmission") + / "plugin_config.yaml", + tech="transmission", +) From ab85770f3ae69113da3c46d0ac253057c261669b Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 3 Jun 2026 08:22:58 -0600 Subject: [PATCH 032/103] clean: --- compass/extraction/transmission/transmission_schema.json5 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index cac429ca2..fc919663f 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -56,12 +56,6 @@ "summary": { "type": "string", "description": "A short summary of the relevant ordinance requiremets." - /* - Byron's - "description": "A short summary with direct ordinance excerpts or quotes whenever possible. For qualitative features such as permitting, fencing, lighting, seismic monitoring, decommissioning, and prohibitions, this is the primary output field and should contain direct ordinance language. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders such as 'No explicit requirement found'; omit the feature instead when no requirement is present." - */ - // "description": "A short summary of the relevant ordinance requirement using direct text excerpts and quotes as much as possible. If multiple options exist and a selection was made, list all other options and their conditions in the summary. For qualitative restrictions, this is the primary output field containing the full extracted text. Can be null if no requirement found." - //"description": "A short summary with direct ordinance excerpts/quotes whenever possible. For qualitative features (definitions, permitting, screening, inspection, decommissioning, prohibitions), this is the primary output field and should contain a direct ordinance excerpt. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders (for example, 'No explicit ... found'); omit the feature instead when no requirement is present." }, "explanation": { "type": "string", From e755435e8a5687d10fc1117bec8f059b375dcea0 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 3 Jun 2026 08:30:50 -0600 Subject: [PATCH 033/103] style: --- compass/qc/schema_eval.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py index 3ee7549c7..2635dc338 100644 --- a/compass/qc/schema_eval.py +++ b/compass/qc/schema_eval.py @@ -52,13 +52,13 @@ class C: """Tiny ANSI colour helpers""" - GREEN = "\033[92m" - RED = "\033[91m" + GREEN = "\033[92m" + RED = "\033[91m" YELLOW = "\033[93m" - CYAN = "\033[96m" - BOLD = "\033[1m" - DIM = "\033[2m" - RESET = "\033[0m" + CYAN = "\033[96m" + BOLD = "\033[1m" + DIM = "\033[2m" + RESET = "\033[0m" @staticmethod def ok(s: str) -> str: return f"{C.GREEN}{s}{C.RESET}" @@ -149,9 +149,9 @@ def cmd_compare( label_a = Path(run_a_path).stem label_b = Path(run_b_path).stem - print(C.bold(f"\n{'='*70}")) + print(C.bold(f"\n{'=' * 70}")) print(C.bold(f" Comparison: {label_a} vs {label_b}")) - print(C.bold(f"{'='*70}\n")) + print(C.bold(f"{'=' * 70}\n")) # Build key sets — tuples of (county, state, subdivision, feature) def key_set(df: pl.DataFrame) -> set[tuple]: @@ -250,9 +250,9 @@ def _filter(df, c, s, sd, f): n_unchanged += 1 # ── Comparison summary ─────────────────────────────────────── - print(C.bold(f"\n{'─'*70}")) + print(C.bold(f"\n{'─' * 70}")) print(C.bold(" Comparison summary")) - print(f"{'─'*70}") + print(f"{'─' * 70}") print(f" Rows only in {label_a}: {len(only_a)}") print(f" Rows only in {label_b}: {len(only_b)}") print(f" Shared rows, changed : {C.warn(str(n_changed))}") @@ -270,9 +270,9 @@ def _print_ref_scoring(ref_path, df_a, df_b, label_a, label_b): """Score both runs against reference and show divergences""" ref = load_reference(ref_path) - print(C.bold(f"{'─'*70}")) + print(C.bold(f"{'─' * 70}")) print(C.bold(" Reference scoring")) - print(f"{'─'*70}\n") + print(f"{'─' * 70}\n") for label, df in [(label_a, df_a), (label_b, df_b)]: passed, total = score_run(ref, df.lazy()) @@ -355,11 +355,11 @@ def cmd_init(run_path: str, output_path: str): grouped: dict[str, dict[str, Any]] = {} for row in df.iter_rows(named=True): - county = row["county"] or "unknown" - state = row["state"] or "unknown" - subdiv = row.get("subdivision") + county = row["county"] or "unknown" + state = row["state"] or "unknown" + subdiv = row.get("subdivision") feature = row["feature"] or "unknown" - fips = row.get("FIPS", "") + fips = row.get("FIPS", "") loc_key = location_label({ "county": county, "state": state, @@ -380,7 +380,7 @@ def cmd_init(run_path: str, output_path: str): feat_entry[fld] = "not_null" grouped[loc_key]["features"][feature] = ( - feat_entry if feat_entry else None + feat_entry or None ) out = Path(output_path) From d5396e2af004d6ba3cc05bcc34d4b11a27310a9f Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 3 Jun 2026 08:32:06 -0600 Subject: [PATCH 034/103] style: --- compass/qc/schema_eval.py | 125 ++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py index 2635dc338..58972fd45 100644 --- a/compass/qc/schema_eval.py +++ b/compass/qc/schema_eval.py @@ -61,13 +61,20 @@ class C: RESET = "\033[0m" @staticmethod - def ok(s: str) -> str: return f"{C.GREEN}{s}{C.RESET}" + def ok(s: str) -> str: + return f"{C.GREEN}{s}{C.RESET}" + @staticmethod - def fail(s: str) -> str: return f"{C.RED}{s}{C.RESET}" + def fail(s: str) -> str: + return f"{C.RED}{s}{C.RESET}" + @staticmethod - def warn(s: str) -> str: return f"{C.YELLOW}{s}{C.RESET}" + def warn(s: str) -> str: + return f"{C.YELLOW}{s}{C.RESET}" + @staticmethod - def bold(s: str) -> str: return f"{C.BOLD}{s}{C.RESET}" + def bold(s: str) -> str: + return f"{C.BOLD}{s}{C.RESET}" # ── Formatting helpers ─────────────────────────────────────────────── @@ -168,20 +175,14 @@ def key_set(df: pl.DataFrame) -> set[tuple]: if only_a or only_b: print(C.bold(" Row presence changes:")) if only_a: - print( - f"\n {C.fail(f'Removed in {label_b}')}" - f" ({len(only_a)}):" - ) + print(f"\n {C.fail(f'Removed in {label_b}')} ({len(only_a)}):") for c, s, sd, f in sorted(only_a, key=_sortable_key): loc = location_label( {"county": c, "state": s, "subdivision": sd} ) print(f" − {loc} → {f}") if only_b: - print( - f"\n {C.ok(f'Added in {label_b}')}" - f" ({len(only_b)}):" - ) + print(f"\n {C.ok(f'Added in {label_b}')} ({len(only_b)}):") for c, s, sd, f in sorted(only_b, key=_sortable_key): loc = location_label( {"county": c, "state": s, "subdivision": sd} @@ -191,8 +192,7 @@ def key_set(df: pl.DataFrame) -> set[tuple]: # ── Field-level diff on shared rows ────────────────────────── compare_fields = [ - f for f in ALL_CHECK_FIELDS - if f in df_a.columns and f in df_b.columns + f for f in ALL_CHECK_FIELDS if f in df_a.columns and f in df_b.columns ] n_changed = 0 n_unchanged = 0 @@ -212,12 +212,12 @@ def _filter(df, c, s, sd, f): mask &= pl.col("subdivision").is_null() return df.filter(mask) - row_a = _filter( - df_a, county, state, subdiv, feature - ).row(0, named=True) - row_b = _filter( - df_b, county, state, subdiv, feature - ).row(0, named=True) + row_a = _filter(df_a, county, state, subdiv, feature).row( + 0, named=True + ) + row_b = _filter(df_b, county, state, subdiv, feature).row( + 0, named=True + ) diffs: list[tuple[str, str | None, str | None]] = [] for fld in compare_fields: @@ -262,7 +262,11 @@ def _filter(df, c, s, sd, f): # ── Optional: score both against reference ─────────────────── if ref_path: _print_ref_scoring( - ref_path, df_a, df_b, label_a, label_b, + ref_path, + df_a, + df_b, + label_a, + label_b, ) @@ -289,10 +293,7 @@ def _print_ref_scoring(ref_path, df_a, df_b, label_a, label_b): print(f" {d['location']} · {d['field']}") sa = C.ok("✓") if d["a_pass"] else C.fail("✗") sb = C.ok("✓") if d["b_pass"] else C.fail("✗") - print( - f" {label_a}: {sa} {label_b}: {sb}" - f" — {d['detail']}" - ) + print(f" {label_a}: {sa} {label_b}: {sb} — {d['detail']}") print() @@ -319,30 +320,46 @@ def _find_divergences( for fld, check in checks.items(): res_a = ( - run_checks(row_a, {fld: check}) if row_a - else [CheckResult( - fld, check["mode"], False, - "", "(missing)", "row missing", - )] + run_checks(row_a, {fld: check}) + if row_a + else [ + CheckResult( + fld, + check["mode"], + False, + "", + "(missing)", + "row missing", + ) + ] ) res_b = ( - run_checks(row_b, {fld: check}) if row_b - else [CheckResult( - fld, check["mode"], False, - "", "(missing)", "row missing", - )] + run_checks(row_b, {fld: check}) + if row_b + else [ + CheckResult( + fld, + check["mode"], + False, + "", + "(missing)", + "row missing", + ) + ] ) if res_a[0].passed != res_b[0].passed: - divs.append({ - "location": feat_label, - "field": fld, - "a_pass": res_a[0].passed, - "b_pass": res_b[0].passed, - "detail": ( - f"A: {res_a[0].actual[:50]}" - f" B: {res_b[0].actual[:50]}" - ), - }) + divs.append( + { + "location": feat_label, + "field": fld, + "a_pass": res_a[0].passed, + "b_pass": res_b[0].passed, + "detail": ( + f"A: {res_a[0].actual[:50]}" + f" B: {res_b[0].actual[:50]}" + ), + } + ) return divs @@ -361,10 +378,13 @@ def cmd_init(run_path: str, output_path: str): feature = row["feature"] or "unknown" fips = row.get("FIPS", "") - loc_key = location_label({ - "county": county, "state": state, - "subdivision": subdiv, - }) + loc_key = location_label( + { + "county": county, + "state": state, + "subdivision": subdiv, + } + ) if loc_key not in grouped: grouped[loc_key] = {"FIPS": fips, "features": {}} @@ -379,14 +399,11 @@ def cmd_init(run_path: str, output_path: str): if v: feat_entry[fld] = "not_null" - grouped[loc_key]["features"][feature] = ( - feat_entry or None - ) + grouped[loc_key]["features"][feature] = feat_entry or None out = Path(output_path) lines = [ - "# Reference template — generated from: " - + Path(run_path).name, + "# Reference template — generated from: " + Path(run_path).name, "# Review each entry and adjust match modes:", '# exact value → value: "1500"', "# keywords → summary:", From e062329924bce185dbc455adfbfce23d56022c00 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 3 Jun 2026 08:37:51 -0600 Subject: [PATCH 035/103] Moving QC module outside core COMPASS Creating a new package just for those support functionalities. --- compass/qc/core.py | 229 ---------------- compass/qc/reference.py | 286 -------------------- compass/qc/schema_eval.py | 538 -------------------------------------- 3 files changed, 1053 deletions(-) delete mode 100644 compass/qc/core.py delete mode 100644 compass/qc/reference.py delete mode 100644 compass/qc/schema_eval.py diff --git a/compass/qc/core.py b/compass/qc/core.py deleted file mode 100644 index ee9e22dc5..000000000 --- a/compass/qc/core.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -core.py — Composable building blocks for matching and validation -""" - -from __future__ import annotations - -import logging -from collections.abc import Generator -from dataclasses import dataclass - -import polars as pl - -from reference import location_label - -logger = logging.getLogger(__name__) - - -# ── Data types ─────────────────────────────────────────────────────── - - -@dataclass -class CheckResult: - """Outcome of a single field-level check""" - - field: str - mode: str - passed: bool - expected: str - actual: str - detail: str = "" - - -# ── Location-level functions ───────────────────────────────────────── - - -def extract_locations( - lf: pl.LazyFrame, -) -> set[tuple[str, str, str | None]]: - """ - Collect distinct locations from a LazyFrame - - Each location is a tuple of ``(state, county, subdivision)`` - where *subdivision* is ``None`` for county-level records. - - Parameters - ---------- - lf : pl.LazyFrame - Lazy representation of run data containing at least - the columns ``state``, ``county``, and - ``subdivision``. - - Returns - ------- - set[tuple[str, str, str | None]] - Unique location tuples found in the data. - """ - rows = ( - lf.select("state", "county", "subdivision") - .unique() - .collect() - .iter_rows() - ) - return {(state, county, subdiv) for state, county, subdiv in rows} - - -def find_missing_locations( - ref: dict[str, dict], - lf: pl.LazyFrame, -) -> list[dict]: - """ - Find reference locations absent from the target - - Compares the locations declared in *ref* against the - distinct locations present in *lf*. Returns the - reference entries whose geographic key (state, county, - subdivision) has no matching rows in the target. - - Parameters - ---------- - ref : dict[str, dict] - Reference dict as returned by - ``reference.load_reference()``. - lf : pl.LazyFrame - Lazy representation of the target run data. - - Returns - ------- - list[dict] - The ``loc_data`` dicts for each reference location - not found in the target, in the order they appear - in *ref*. Empty list when all reference locations - are present. - """ - target_locs = extract_locations(lf) - - missing = [] - for _loc_key, loc_data in ref.items(): - loc_tuple = ( - loc_data["state"], - loc_data["county"], - loc_data.get("subdivision"), - ) - if loc_tuple not in target_locs: - missing.append(loc_data) - return missing - - -def match_labels( - ref: dict[str, dict], - lf: pl.LazyFrame, -) -> Generator[tuple[dict, pl.DataFrame], None, None]: - """Pair ground-truth locations with matching run rows - - Iterates over each location in *ref*, builds a - geographic filter (state, county, and subdivision when - defined), applies it to *lf*, and collects the result. - When the reference entry declares a FIPS code, the matched - rows are checked for agreement; mismatches are reported - via ``logging.error``. - - Parameters - ---------- - ref : dict[str, dict] - Reference dict as returned by - ``reference.load_reference()``, keyed by normalised - location string. - lf : pl.LazyFrame - Lazy representation of a run CSV, as produced by - ``load_run(path).lazy()``. - - Yields - ------ - tuple[dict, pl.DataFrame] - A pair ``(loc_data, loc_df)`` for each location in - *ref*: - - loc_data - The reference dict for one location, containing - state, county, subdivision, FIPS, and features - with their check specs. - loc_df - Collected DataFrame with every run row that - matches the location geographically. May be - empty when the run has no data for that - location. - """ - for _loc_key, loc_data in ref.items(): - mask = ( - (pl.col("county") == loc_data["county"]) - & (pl.col("state") == loc_data["state"]) - ) - subdiv = loc_data.get("subdivision") - if subdiv: - mask = mask & (pl.col("subdivision") == subdiv) - else: - mask = mask & pl.col("subdivision").is_null() - - loc_df = lf.filter(mask).collect() - - # Validate FIPS agreement - expected_fips = loc_data.get("FIPS") - if expected_fips is not None and not loc_df.is_empty(): - run_fips = loc_df["FIPS"].unique().to_list() - mismatched = [ - f for f in run_fips - if f is not None and f != expected_fips - ] - if mismatched: - loc_lbl = location_label(loc_data) - logger.error( - "FIPS mismatch for %s: reference declares %s, " - "run contains %s", - loc_lbl, expected_fips, mismatched, - ) - - yield loc_data, loc_df - - -def find_missing_features( - loc_data: dict, - lf: pl.LazyFrame, -) -> list[str]: - """ - Find features declared in the reference but absent from the run. - - Compares the feature names listed in *loc_data* against - the distinct ``feature`` values present in *lf*. Any - feature that appears in the reference but has no matching - row in the run is considered missing. - - Designed to compose with :func:`match_labels`:: - - for loc_data, loc_df in match_labels(ref, run_lf): - missing = find_missing_features( - loc_data, loc_df.lazy(), - ) - - Parameters - ---------- - loc_data : dict - Truth dict for a single location, as yielded by - :func:`match_labels`. Must contain a ``features`` - key mapping feature names to check specs (which - may be empty dicts for presence-only features). - lf : pl.LazyFrame - Lazy representation of the run rows already scoped - to this location. - - Returns - ------- - list[str] - Feature names present in the reference but not found - in the run, in the order they appear in - ``loc_data["features"]``. Empty list when all - features are present. - """ - expected = set(loc_data.get("features", {}).keys()) - if not expected: - return [] - - present = set( - lf.select("feature") - .unique() - .collect() - .get_column("feature") - .to_list() - ) - - return [f for f in loc_data["features"] if f not in present] diff --git a/compass/qc/reference.py b/compass/qc/reference.py deleted file mode 100644 index dc6f0a11b..000000000 --- a/compass/qc/reference.py +++ /dev/null @@ -1,286 +0,0 @@ -""" -reference.py — Load, merge, and validate reference YAML files. - -Handles single files, directories (recursive), and duplicate detection -both within and across files. - -Location keys support two granularities: - - County level : "County, State" → e.g. "Power, Idaho" - Township level: "Subdivision, County, State" → e.g. "Springfield, Power, Idaho" - -load_reference() returns a dict grouped by location rather than a flat list, -so downstream code can process and summarise per-location. -""" - -from __future__ import annotations - -from pathlib import Path - -import yaml - -# ── Field definitions ──────────────────────────────────────────────────────── -# These define which CSV columns the reference format understands. - -# Columns where exact string match is the natural comparison -EXACT_FIELDS = ["value", "units", "adder", "min_dist", "max_dist", "year"] - -# Columns where text / keyword / not-null matching makes more sense -TEXT_FIELDS = ["summary", "section", "source"] - -ALL_CHECK_FIELDS = EXACT_FIELDS + TEXT_FIELDS - -# ── Exceptions ─────────────────────────────────────────────────────────────── - - -class DuplicateLocationError(Exception): - """Raised when the same location key appears in more than one place.""" - - -# ── Location key parsing ──────────────────────────────────────────────────── - - -def parse_location_key(key: str) -> dict[str, str | None]: - """ - Parse a YAML location key into its component parts. - - Supports: - "County, State" → county-level - "Subdivision, County, State" → township-level - - Returns a dict with keys: state, county, subdivision (None if county-level). - """ - parts = [p.strip() for p in key.split(",")] - - if len(parts) == 2: - return { - "state": parts[1].lower(), - "county": parts[0].lower(), - "subdivision": None, - } - elif len(parts) == 3: - return { - "state": parts[2].lower(), - "county": parts[1].lower(), - "subdivision": parts[0].lower(), - } - else: - raise ValueError( - f"Location key must have 2 parts (County, State) or " - f"3 parts (Subdivision, County, State), got {len(parts)}: '{key}'" - ) - - -def location_label(loc: dict[str, str | None]) -> str: - """ - Build a human-readable label from parsed location components. - - Returns "County, State" or "Subdivision, County, State". - """ - parts = [] - if loc.get("subdivision"): - parts.append(loc["subdivision"].title()) - parts.append(loc["county"].title()) - parts.append(loc["state"].title()) - return ", ".join(parts) - - -# ── File collection ────────────────────────────────────────────────────────── - - -def collect_reference_files(path: str | Path) -> list[Path]: - """ - Walk *path* and return every .yaml / .yml file found. - - If *path* is a single file, return it in a one-element list. - If *path* is a directory, recurse into all sub-folders (sorted for - deterministic ordering). - """ - p = Path(path) - if p.is_file(): - return [p] - if p.is_dir(): - files = sorted(p.rglob("*.yaml")) + sorted(p.rglob("*.yml")) - # rglob may return .yml files that also matched .yaml; deduplicate - seen: set[Path] = set() - unique: list[Path] = [] - for f in files: - resolved = f.resolve() - if resolved not in seen: - seen.add(resolved) - unique.append(f) - return unique - raise FileNotFoundError(f"Reference path not found: {p}") - - -# ── Merging & duplicate detection ──────────────────────────────────────────── - - -def merge_reference_dicts(files: list[Path]) -> dict: - """ - Load every YAML file and merge into one dict. - - Raises DuplicateLocationError if a top-level location key (e.g. - "Power, Idaho") appears more than once — whether across different - files or duplicated inside the same file. - """ - merged: dict = {} - # Track where each key was first seen for the error message - origin: dict[str, Path] = {} - - for fpath in files: - raw = yaml.safe_load(fpath.read_text()) - if raw is None: - continue - if not isinstance(raw, dict): - raise ValueError( - f"Expected a YAML mapping at the top level of {fpath}, " - f"got {type(raw).__name__}" - ) - - # Check for intra-file duplicates. PyYAML silently keeps the last - # occurrence when a key is repeated, so we do a quick text-level - # scan to catch that case before it's swallowed. - _check_intra_file_duplicates(fpath) - - for key in raw: - norm = _normalise_location_key(key) - if norm in origin: - raise DuplicateLocationError( - f"Duplicate location '{key}' — already defined in " - f"{origin[norm]}, found again in {fpath}" - ) - origin[norm] = fpath - merged[key] = raw[key] - - return merged - - -def _normalise_location_key(key: str) -> str: - """Lowercase + strip so 'Power, Idaho' and ' power , idaho ' collide.""" - return ", ".join(p.strip().lower() for p in key.split(",")) - - -def _check_intra_file_duplicates(fpath: Path) -> None: - """ - Detect duplicate top-level keys inside a single YAML file. - - PyYAML's safe_load silently drops all-but-the-last duplicate key, - so we scan the raw text for top-level keys (lines that start at - column 0 and end with ':') and flag repeats. - """ - seen: dict[str, int] = {} - for lineno, line in enumerate(fpath.read_text().splitlines(), start=1): - stripped = line.rstrip() - # Skip blank lines, comments, and indented lines - if not stripped or stripped.startswith("#") or line[0] in (" ", "\t"): - continue - # A top-level key line looks like `"Power, Idaho":` or `Power, Idaho:` - if stripped.endswith(":"): - raw_key = stripped[:-1].strip().strip('"').strip("'") - norm = _normalise_location_key(raw_key) - if norm in seen: - raise DuplicateLocationError( - f"Duplicate location '{raw_key}' inside {fpath} " - f"(lines {seen[norm]} and {lineno})" - ) - seen[norm] = lineno - - -# ── Check-spec builder (internal) ─────────────────────────────────────────── - - -def _build_checks(field_checks: dict) -> dict[str, dict]: - """ - Convert a raw YAML feature block into a checks dict. - - Each key is a field name, each value is a dict describing the match mode. - """ - checks: dict[str, dict] = {} - for fld, spec in field_checks.items(): - if fld not in ALL_CHECK_FIELDS: - continue - if isinstance(spec, dict) and "keywords" in spec: - checks[fld] = { - "mode": "keywords", - "keywords": [str(k).lower() for k in spec["keywords"]], - } - elif spec == "not_null": - checks[fld] = {"mode": "not_null"} - elif spec == "absent": - checks[fld] = {"mode": "absent"} - else: - checks[fld] = {"mode": "exact", "expected": str(spec).strip().lower()} - return checks - - -# ── Main loader ────────────────────────────────────────────────────────────── - - -def load_reference(path: str | Path) -> dict[str, dict]: - """ - Parse reference YAML(s) into a dict grouped by location. - - *path* can be: - - a single .yaml / .yml file - - a directory — every .yaml / .yml underneath is collected and merged - - Raises DuplicateLocationError if any location key appears more than once. - - Returns a dict keyed by normalised location string:: - - { - "power, idaho": { - "state": "idaho", - "county": "power", - "subdivision": None, - "FIPS": "16077", - "features": { - "residential buildings": { - "value": {"mode": "exact", "expected": "1500"}, - "units": {"mode": "exact", "expected": "feet"}, - "summary": {"mode": "keywords", "keywords": [...]}, - ... - }, - "property lines": { ... }, - } - }, - "springfield, power, idaho": { - "state": "idaho", - "county": "power", - "subdivision": "springfield", - ... - }, - } - """ - files = collect_reference_files(path) - if not files: - raise FileNotFoundError(f"No .yaml / .yml files found under {path}") - - raw = merge_reference_dicts(files) - result: dict[str, dict] = {} - - for location_key, loc_data in raw.items(): - loc = parse_location_key(location_key) - norm_key = _normalise_location_key(location_key) - - fips = loc_data.get("FIPS") - raw_features = loc_data.get("features", {}) - - parsed_features: dict[str, dict] = {} - for feat_name, field_checks in raw_features.items(): - if field_checks is None: - # Feature listed with no arguments — still - # track it so presence checks can catch it. - parsed_features[feat_name.lower()] = {} - continue - checks = _build_checks(field_checks) - parsed_features[feat_name.lower()] = checks - - result[norm_key] = { - **loc, - "FIPS": str(fips) if fips is not None else None, - "features": parsed_features, - } - - return result diff --git a/compass/qc/schema_eval.py b/compass/qc/schema_eval.py deleted file mode 100644 index 58972fd45..000000000 --- a/compass/qc/schema_eval.py +++ /dev/null @@ -1,538 +0,0 @@ -""" -schema_eval.py — CLI entry point for extraction evaluation - -Subcommands ------------ - init Scaffold a reference YAML from an existing CSV run. - validate Score one CSV run against reference. - compare Diff two CSV runs; optionally score both against reference. - -Examples --------- - python schema_eval.py init run1.csv -o reference.yaml - python schema_eval.py validate run1.csv -t reference.yaml - python schema_eval.py compare run1.csv run2.csv -t reference.yaml -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import click -import polars as pl - -from core import ( - CheckResult, - extract_locations, - find_feature_row, - find_missing_features, - find_missing_locations, - load_run, - match_labels, - run_checks, - score_run, - validate_formated, -) -from reference import ( - ALL_CHECK_FIELDS, - EXACT_FIELDS, - TEXT_FIELDS, - load_reference, - location_label, -) - -# ── Constants ──────────────────────────────────────────────────────── - -KEY_COLS = ["county", "state", "subdivision", "feature"] - -# ── ANSI helpers ───────────────────────────────────────────────────── - - -class C: - """Tiny ANSI colour helpers""" - - GREEN = "\033[92m" - RED = "\033[91m" - YELLOW = "\033[93m" - CYAN = "\033[96m" - BOLD = "\033[1m" - DIM = "\033[2m" - RESET = "\033[0m" - - @staticmethod - def ok(s: str) -> str: - return f"{C.GREEN}{s}{C.RESET}" - - @staticmethod - def fail(s: str) -> str: - return f"{C.RED}{s}{C.RESET}" - - @staticmethod - def warn(s: str) -> str: - return f"{C.YELLOW}{s}{C.RESET}" - - @staticmethod - def bold(s: str) -> str: - return f"{C.BOLD}{s}{C.RESET}" - - -# ── Formatting helpers ─────────────────────────────────────────────── - - -def _truncate(s: str | None, n: int) -> str: - """Shorten a string for display""" - if s is None: - return "(null)" - return s[:n] + "…" if len(s) > n else s - - -def _pct_color(pct: float) -> callable: - """Pick a colour function based on percentage thresholds""" - if pct >= 90: - return C.ok - return C.warn if pct >= 70 else C.fail - - -def _sortable_key(t: tuple) -> tuple: - """Replace None with '' so tuples are sortable""" - return tuple(v if v is not None else "" for v in t) - - -# ── Validate subcommand ────────────────────────────────────────────── - - -def cmd_validate( - run_path: str, - ref_path: str, - verbose: bool = False, - output_format: str = "text", -): - """Validate a run against reference and return formatted output - - Parameters - ---------- - run_path : str - Path to the CSV run file to validate. - ref_path : str - Path to the reference YAML file or directory. - verbose : bool, default=False - Include passing checks in text output. By default, False. - output_format : str, default="text" - Output format to render. Supported values are ``"text"`` - and ``"json"``. By default, text. - - Returns - ------- - str - Rendered validation report as text or JSON string. - """ - lf = load_run(run_path) - ref = load_reference(ref_path) - - return validate_formated( - ref, - lf, - run_path, - ref_path, - output_format=output_format, - verbose=verbose, - style=C, - ) - - -# ── Compare subcommand ─────────────────────────────────────────────── - - -def cmd_compare( - run_a_path: str, - run_b_path: str, - ref_path: str | None = None, - verbose: bool = False, -): - df_a = load_run(run_a_path).collect() - df_b = load_run(run_b_path).collect() - - label_a = Path(run_a_path).stem - label_b = Path(run_b_path).stem - - print(C.bold(f"\n{'=' * 70}")) - print(C.bold(f" Comparison: {label_a} vs {label_b}")) - print(C.bold(f"{'=' * 70}\n")) - - # Build key sets — tuples of (county, state, subdivision, feature) - def key_set(df: pl.DataFrame) -> set[tuple]: - return set(df.select(KEY_COLS).unique().iter_rows()) - - keys_a = key_set(df_a) - keys_b = key_set(df_b) - - only_a = keys_a - keys_b - only_b = keys_b - keys_a - common = keys_a & keys_b - - # ── Row-presence diff ──────────────────────────────────────── - if only_a or only_b: - print(C.bold(" Row presence changes:")) - if only_a: - print(f"\n {C.fail(f'Removed in {label_b}')} ({len(only_a)}):") - for c, s, sd, f in sorted(only_a, key=_sortable_key): - loc = location_label( - {"county": c, "state": s, "subdivision": sd} - ) - print(f" − {loc} → {f}") - if only_b: - print(f"\n {C.ok(f'Added in {label_b}')} ({len(only_b)}):") - for c, s, sd, f in sorted(only_b, key=_sortable_key): - loc = location_label( - {"county": c, "state": s, "subdivision": sd} - ) - print(f" + {loc} → {f}") - print() - - # ── Field-level diff on shared rows ────────────────────────── - compare_fields = [ - f for f in ALL_CHECK_FIELDS if f in df_a.columns and f in df_b.columns - ] - n_changed = 0 - n_unchanged = 0 - - for key in sorted(common, key=_sortable_key): - county, state, subdiv, feature = key - - def _filter(df, c, s, sd, f): - mask = ( - (pl.col("county") == c) - & (pl.col("state") == s) - & (pl.col("feature") == f) - ) - if sd: - mask &= pl.col("subdivision") == sd - else: - mask &= pl.col("subdivision").is_null() - return df.filter(mask) - - row_a = _filter(df_a, county, state, subdiv, feature).row( - 0, named=True - ) - row_b = _filter(df_b, county, state, subdiv, feature).row( - 0, named=True - ) - - diffs: list[tuple[str, str | None, str | None]] = [] - for fld in compare_fields: - va = row_a.get(fld) - vb = row_b.get(fld) - na = va.strip().lower() if va else None - nb = vb.strip().lower() if vb else None - if na != nb: - diffs.append((fld, va, vb)) - - loc = location_label( - {"county": county, "state": state, "subdivision": subdiv} - ) - label = f"{loc} → {feature}" - - if diffs: - n_changed += 1 - print(f" {C.warn('CHANGED')} {label}") - for fld, va, vb in diffs: - va_d = _truncate(va, 40) if va else "(null)" - vb_d = _truncate(vb, 40) if vb else "(null)" - print( - f" {C.DIM}├─{C.RESET} {fld}:" - f" {C.fail(va_d)} → {C.ok(vb_d)}" - ) - elif verbose: - n_unchanged += 1 - print(f" {C.DIM}SAME{C.RESET} {label}") - else: - n_unchanged += 1 - - # ── Comparison summary ─────────────────────────────────────── - print(C.bold(f"\n{'─' * 70}")) - print(C.bold(" Comparison summary")) - print(f"{'─' * 70}") - print(f" Rows only in {label_a}: {len(only_a)}") - print(f" Rows only in {label_b}: {len(only_b)}") - print(f" Shared rows, changed : {C.warn(str(n_changed))}") - print(f" Shared rows, same : {n_unchanged}") - print() - - # ── Optional: score both against reference ─────────────────── - if ref_path: - _print_ref_scoring( - ref_path, - df_a, - df_b, - label_a, - label_b, - ) - - -def _print_ref_scoring(ref_path, df_a, df_b, label_a, label_b): - """Score both runs against reference and show divergences""" - ref = load_reference(ref_path) - - print(C.bold(f"{'─' * 70}")) - print(C.bold(" Reference scoring")) - print(f"{'─' * 70}\n") - - for label, df in [(label_a, df_a), (label_b, df_b)]: - passed, total = score_run(ref, df.lazy()) - pct = (passed / total * 100) if total else 0 - clr = _pct_color(pct) - print(f" {label:.<40s} {clr(f'{passed}/{total}')} ({pct:.1f}%)") - - print() - - divergences = _find_divergences(ref, df_a, df_b) - if divergences: - print(f" {C.bold('Divergent reference results')}:\n") - for d in divergences: - print(f" {d['location']} · {d['field']}") - sa = C.ok("✓") if d["a_pass"] else C.fail("✗") - sb = C.ok("✓") if d["b_pass"] else C.fail("✗") - print(f" {label_a}: {sa} {label_b}: {sb} — {d['detail']}") - print() - - -def _find_divergences( - ref: dict[str, dict], - df_a: pl.DataFrame, - df_b: pl.DataFrame, -) -> list[dict]: - """Find checks where two runs disagree against the reference""" - slices_a = { - location_label(loc_data): loc_df - for loc_data, loc_df in match_labels(ref, df_a.lazy()) - } - - divs = [] - for loc_data, loc_df_b in match_labels(ref, df_b.lazy()): - loc_lbl = location_label(loc_data) - loc_df_a = slices_a.get(loc_lbl, pl.DataFrame()) - - for feat_name, checks in loc_data["features"].items(): - feat_label = f"{loc_lbl} → {feat_name}" - row_a = find_feature_row(loc_df_a, feat_name) - row_b = find_feature_row(loc_df_b, feat_name) - - for fld, check in checks.items(): - res_a = ( - run_checks(row_a, {fld: check}) - if row_a - else [ - CheckResult( - fld, - check["mode"], - False, - "", - "(missing)", - "row missing", - ) - ] - ) - res_b = ( - run_checks(row_b, {fld: check}) - if row_b - else [ - CheckResult( - fld, - check["mode"], - False, - "", - "(missing)", - "row missing", - ) - ] - ) - if res_a[0].passed != res_b[0].passed: - divs.append( - { - "location": feat_label, - "field": fld, - "a_pass": res_a[0].passed, - "b_pass": res_b[0].passed, - "detail": ( - f"A: {res_a[0].actual[:50]}" - f" B: {res_b[0].actual[:50]}" - ), - } - ) - return divs - - -# ── Init subcommand ────────────────────────────────────────────────── - - -def cmd_init(run_path: str, output_path: str): - """Generate a reference YAML template from an existing CSV run""" - df = load_run(run_path).collect() - - grouped: dict[str, dict[str, Any]] = {} - for row in df.iter_rows(named=True): - county = row["county"] or "unknown" - state = row["state"] or "unknown" - subdiv = row.get("subdivision") - feature = row["feature"] or "unknown" - fips = row.get("FIPS", "") - - loc_key = location_label( - { - "county": county, - "state": state, - "subdivision": subdiv, - } - ) - - if loc_key not in grouped: - grouped[loc_key] = {"FIPS": fips, "features": {}} - - feat_entry: dict[str, Any] = {} - for fld in EXACT_FIELDS: - v = row.get(fld) - if v: - feat_entry[fld] = v - for fld in TEXT_FIELDS: - v = row.get(fld) - if v: - feat_entry[fld] = "not_null" - - grouped[loc_key]["features"][feature] = feat_entry or None - - out = Path(output_path) - lines = [ - "# Reference template — generated from: " + Path(run_path).name, - "# Review each entry and adjust match modes:", - '# exact value → value: "1500"', - "# keywords → summary:", - "# keywords: [word1, word2]", - "# not_null → section: not_null", - "# absent → adder: absent", - "# remove line → field won't be checked", - "#", - "# Location keys:", - '# County level → "County, State"', - '# Township level → "Subdivision, County, State"', - "", - ] - - for loc_key in sorted(grouped): - data = grouped[loc_key] - lines.append(f'"{loc_key}":') - if data["FIPS"]: - lines.append(f' FIPS: "{data["FIPS"]}"') - lines.append(" features:") - for feat_name in sorted(data["features"]): - lines.append("") - lines.append(f" {feat_name}:") - feat = data["features"][feat_name] - if feat is None: - lines.append(" # (no fields extracted)") - continue - for fld, val in feat.items(): - if val == "not_null": - lines.append(f" {fld}: not_null") - else: - lines.append(f' {fld}: "{val}"') - lines.append("") - - out.write_text("\n".join(lines)) - print(f"\n {C.ok('✓')} Template written to {C.bold(str(out))}") - print( - f" {C.DIM}Edit the file to set expected values" - f" and match modes.{C.RESET}\n" - ) - - -# ── CLI ────────────────────────────────────────────────────────────── - - -@click.group( - epilog=__doc__, - context_settings={"help_option_names": ["-h", "--help"]}, -) -def main(): - """Evaluate and compare LLM extraction runs against reference""" - - -@main.command("init") -@click.argument("run") -@click.option( - "-o", - "--output", - "output_path", - default="ground_truth.yaml", - show_default=True, - help="Output YAML path", -) -def init_command(run: str, output_path: str): - """Scaffold reference YAML from a CSV run""" - cmd_init(run, output_path) - - -@main.command("validate") -@click.argument("run") -@click.option( - "-t", - "--ref", - "ref_path", - required=True, - help="Path to reference YAML file or directory", -) -@click.option( - "-v", - "--verbose", - is_flag=True, - default=False, - help="Show passing checks too", -) -@click.option( - "-f", - "--format", - "output_format", - type=click.Choice(["text", "json"]), - default="text", - show_default=True, - help="Output format for validation report", -) -def validate_command( - run: str, - ref_path: str, - verbose: bool, - output_format: str, -): - """Validate a CSV run against reference""" - print(cmd_validate(run, ref_path, verbose, output_format)) - - -@main.command("compare") -@click.argument("run_a") -@click.argument("run_b") -@click.option( - "-t", - "--ref", - "ref_path", - default=None, - help="Optional reference YAML file or directory", -) -@click.option( - "-v", - "--verbose", - is_flag=True, - default=False, - help="Show unchanged rows too", -) -def compare_command( - run_a: str, - run_b: str, - ref_path: str | None, - verbose: bool, -): - """Compare two CSV runs""" - cmd_compare(run_a, run_b, ref_path, verbose) - - -if __name__ == "__main__": - main() From 6cf1025e3a6f20a18289777795d78aeed1449313 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 9 Jun 2026 08:09:55 -0600 Subject: [PATCH 036/103] good_tech_acronyms --- compass/extraction/transmission/plugin_config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 341da9fa4..32a4e852b 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -32,6 +32,13 @@ website_keywords: government: 180 heuristic_keywords: true +heuristic_keywords: + good_tech_acronyms: + - "kv" + - "hvdc" + - "ehv" + - "uhv" + - "cpcn" collection_prompts: true From b234b7f4088886fbfd3c21a8b199408036878af1 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 9 Jun 2026 08:10:29 -0600 Subject: [PATCH 037/103] Split context in query_templates Some jurisdictions names can get confused with technical words, such as Power county. The comma helps to split the context. --- compass/extraction/transmission/plugin_config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 32a4e852b..d07d50d05 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -3,11 +3,11 @@ schema: ./transmission_schema.json5 data_type_short_desc: high-voltage transmission lines ordinance query_templates: - - "filetype:pdf {jurisdiction} electric transmission line ordinance" - - "{jurisdiction} high voltage transmission line zoning ordinance" - - "{jurisdiction} electric transmission line siting code" - - "{jurisdiction} overhead electric utility line ordinance" - - "{jurisdiction} transmission line right-of-way regulation" + - "filetype:pdf {jurisdiction}, electric transmission line ordinance" + - "{jurisdiction}, high voltage transmission line zoning ordinance" + - "{jurisdiction}, electric transmission line siting code" + - "{jurisdiction}, overhead electric utility line ordinance" + - "{jurisdiction}, transmission line right-of-way regulation" website_keywords: pdf: 92160 From 6f9883d19d57f5c28c1ec13720a1e63fcb7f72ed Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Tue, 9 Jun 2026 09:35:59 -0600 Subject: [PATCH 038/103] Confirmed good_tech_phrases --- .../extraction/transmission/plugin_config.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index d07d50d05..ef3f9fa3c 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -39,6 +39,22 @@ heuristic_keywords: - "ehv" - "uhv" - "cpcn" + good_tech_phrases: + - "high voltage" + - "high-voltage" + - "extra high voltage" + - "extra-high voltage" + - "bulk transmission" + - "electric transmission" + - "69 kv" + - "115 kv" + - "138 kv" + - "161 kv" + - "230 kv" + - "345 kv" + - "500 kv" + - "765 kv" + collection_prompts: true From 6856df58ac2d324acd35a49d0a8813fd70a24800 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 17 Jun 2026 21:20:52 -0600 Subject: [PATCH 039/103] not_tech: transmission fluid An actual case with Webster Parish --- compass/extraction/transmission/plugin_config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index ef3f9fa3c..03ad81ad6 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -54,6 +54,8 @@ heuristic_keywords: - "345 kv" - "500 kv" - "765 kv" + not_tech_words: + - "transmission fluid" collection_prompts: true From 14b68c832c015e75461c83d4e08676e542a5669e Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 18 Jun 2026 12:35:13 -0600 Subject: [PATCH 040/103] Extending non-tech radio tower gas transmission --- compass/extraction/transmission/plugin_config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 03ad81ad6..326dde0f5 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -56,6 +56,8 @@ heuristic_keywords: - "765 kv" not_tech_words: - "transmission fluid" + - "radio tower" + - "gas transmission" collection_prompts: true From b7cf3582b7a1a071b73eab739b51deda1d4b08a4 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 5 Jul 2026 10:06:01 -0600 Subject: [PATCH 041/103] All original information to collect --- .../transmission/transmission_schema.json5 | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index fc919663f..7ec8e76ea 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -1,7 +1,7 @@ { "title": "Transmission Ordinance Extraction Schema", "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all relevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", - "version": "0.0.1", + "version": "0.0.2", "type": "object", "required": ["outputs"], "additionalProperties": false, @@ -25,11 +25,74 @@ "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", "enum": [ + // -- Setbacks -- + // Do I need to include participating and non-participating + "structures", + // Occupied Dwellings or Residential Buildings + "residential buildings", "property lines", + // Roads of frewways + "roads", + // Heliports and Airport Runways + "airport", + // Ordinary High Water Mark (OHWM) + "OHWM", + "railroads", + + // -- Definitions -- + // Definition /voltage threshold to qualify as a "transmission line" + "voltage threshold", + + // -- Numerical Restrictions -- + // Electric Fields Strength at Property Line + "electric field", // Tower vs facility height. Do I want both? "maximum height", "noise", + + // -- Operational -- + // Bond Requirement + "bond", + // Decommissioning or Remediation Requirements "decommissioning", + // Conditional Use or Special Use Permit Requirement + "permit", + // Compliance with Federal Aviation Administration and local approvals near airports + "FAA", + + // -- Prohibited or limited-use districs -- + // Maybe call these zones, such as rural zone? + "rural zone", + "residential zone", + "commercial zone", + "light industrial zone", + // Includes irrigated farmland, ranch land. + "agricultural zone", + // -- Exclusions -- + "historic site", + "critical species habitat", + "wetland", + "floodplain", + "residence", + "hospital", + "school", + // Daycare centers + "daycare", + // institutions of higher education + "higher education", + + // -- Qualitative -- + "color", + "fencing", + // Fire Prevention or Fire Protection + "fire prevention", + "lighting", + // Identification or Signage ("Danger", "No Trespassing",etc.) + "signage", + // Traffic Disruption + "traffic disruption", + // Vegetation Protection or Landscaping Requirements + "vegetation", ] }, "value": { From 1a11efe1b0b34499d14fcb3ff8fe4fcfe7815bf5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 5 Jul 2026 22:20:04 -0600 Subject: [PATCH 042/103] Extending query_template Adding explicit search for municode and amlegal, as two good sources. --- compass/extraction/transmission/plugin_config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 326dde0f5..690a5a1b3 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -3,6 +3,8 @@ schema: ./transmission_schema.json5 data_type_short_desc: high-voltage transmission lines ordinance query_templates: + - "site:municode.com {jurisdiction}, electrical transmission line" + - "site:codelibrary.amlegal.com {jurisdiction}, electrical transmission line" - "filetype:pdf {jurisdiction}, electric transmission line ordinance" - "{jurisdiction}, high voltage transmission line zoning ordinance" - "{jurisdiction}, electric transmission line siting code" From 329114af544cb9b12bc190575afd750d2d1174c4 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 5 Jul 2026 22:21:31 -0600 Subject: [PATCH 043/103] Minimalist good_tech_keywords It has been working fine. --- compass/extraction/transmission/plugin_config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 690a5a1b3..26eafd72b 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -35,6 +35,9 @@ website_keywords: heuristic_keywords: true heuristic_keywords: + good_tech_keywords: + - "transmission" + - "powerline" good_tech_acronyms: - "kv" - "hvdc" From a773be91bb715141a4539486f25188ae04f1aa19 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 5 Jul 2026 22:22:06 -0600 Subject: [PATCH 044/103] doc: Note on CPCN Certificate of Public Convenience and Necessity --- compass/extraction/transmission/plugin_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 26eafd72b..8206a8647 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -43,7 +43,7 @@ heuristic_keywords: - "hvdc" - "ehv" - "uhv" - - "cpcn" + - "cpcn" # Certificate of Public Convenience and Necessity good_tech_phrases: - "high voltage" - "high-voltage" From a208077a5c947d7f2138d58fc2de302c9b1a246c Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 5 Jul 2026 22:22:53 -0600 Subject: [PATCH 045/103] Extending doc selection to multi-doc-mixed It sounds like the right strategy, in particular due to franchise documents, but it doesn't look like it's working correctly. Maybe the n-top=5 is too limiting. --- compass/extraction/transmission/plugin_config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 8206a8647..f71bba6af 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -2,6 +2,8 @@ schema: ./transmission_schema.json5 data_type_short_desc: high-voltage transmission lines ordinance +doc_selection_method: "multi doc mixed" + query_templates: - "site:municode.com {jurisdiction}, electrical transmission line" - "site:codelibrary.amlegal.com {jurisdiction}, electrical transmission line" From 789d0b862f60bbb206c0195f7f3a32268c110cc0 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 14:13:45 -0600 Subject: [PATCH 046/103] feat: Adding `scope` to parsed fields --- compass/utilities/finalize.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index 975dae175..12f449192 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -16,6 +16,30 @@ logger = logging.getLogger(__name__) +_PARSED_COLS = [ + # TODO: Put these in an enum + "county", + "state", + "subdivision", + "jurisdiction_type", + "FIPS", + "feature", + "scope", + "value", + "units", + "adder", + "min_dist", + "max_dist", + "summary", + "year", + "section", + "source", + "quantitative", +] +QUANT_OUT_COLS = _PARSED_COLS[:-1] +"""Output columns in quantitative ordinance file""" +QUAL_OUT_COLS = _PARSED_COLS[:7] + _PARSED_COLS[-5:-1] +"""Output columns in qualitative ordinance file""" def save_run_meta( From 55878bfdbbbc0ec55392f8d25f96c45a38bd695f Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 14:22:22 -0600 Subject: [PATCH 047/103] feat: Inject sentinel values if scope is defined --- compass/plugin/one_shot/base.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index 59b683372..9b639ed26 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -52,6 +52,29 @@ class _CacheKey(StrEnum): HEURISTIC_KEYWORDS = auto() +SCOPE_SENTINEL_ALL = "all" +SCOPE_SENTINEL_OTHER = "other" + + +def _inject_scope_sentinels(schema): + """Prepend ``all`` and append ``other`` to the scope enum in-place""" + try: + scope = schema["properties"]["outputs"]["items"]["properties"][ + "scope" + ] + except (KeyError, TypeError): + return + + enum = scope.get("enum") + if not isinstance(enum, list): + return + + if SCOPE_SENTINEL_ALL not in enum: + enum.insert(0, SCOPE_SENTINEL_ALL) + if SCOPE_SENTINEL_OTHER not in enum: + enum.append(SCOPE_SENTINEL_OTHER) + + def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore[complex-structure] """Create a one-shot extraction plugin based on a configuration @@ -193,6 +216,8 @@ def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore if isinstance(config["schema"], str): config["schema"] = load_config(config["schema"]) + _inject_scope_sentinels(config["schema"]) + config["qual_feats"] = { f.casefold() for f in config["schema"].pop("$qualitative_features", []) } From b8481634cf35e001f1d18067fc8f9225e127b0f3 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:19:40 -0600 Subject: [PATCH 048/103] Adding `power line` as suggested by Paul --- compass/extraction/transmission/plugin_config.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index f71bba6af..6507c2bba 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -8,10 +8,11 @@ query_templates: - "site:municode.com {jurisdiction}, electrical transmission line" - "site:codelibrary.amlegal.com {jurisdiction}, electrical transmission line" - "filetype:pdf {jurisdiction}, electric transmission line ordinance" - - "{jurisdiction}, high voltage transmission line zoning ordinance" - - "{jurisdiction}, electric transmission line siting code" - - "{jurisdiction}, overhead electric utility line ordinance" - - "{jurisdiction}, transmission line right-of-way regulation" + - "{jurisdiction}, zoning code electric transmission line setback" + - "{jurisdiction}, high voltage transmission line ordinance" + - "{jurisdiction}, overhead electric utility line zoning" + - "{jurisdiction}, power line" + website_keywords: pdf: 92160 From 65e76f1eaf3a27cd796dd37d2a2ac798bf557446 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:20:11 -0600 Subject: [PATCH 049/103] Extending queries to target franchise agreements --- compass/extraction/transmission/plugin_config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 6507c2bba..4149d4443 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -12,6 +12,9 @@ query_templates: - "{jurisdiction}, high voltage transmission line ordinance" - "{jurisdiction}, overhead electric utility line zoning" - "{jurisdiction}, power line" + - "{jurisdiction}, electric utility franchise ordinance" + - "{jurisdiction}, code of ordinances electric franchise" + - "filetype:pdf {jurisdiction} electric franchise agreement" website_keywords: From 10430ac2fad9849d5cf000967f0a5c31302e6fb0 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:22:49 -0600 Subject: [PATCH 050/103] Expanding keywords --- .../extraction/transmission/plugin_config.yaml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 4149d4443..7ff4c50c4 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -16,7 +16,6 @@ query_templates: - "{jurisdiction}, code of ordinances electric franchise" - "filetype:pdf {jurisdiction} electric franchise agreement" - website_keywords: pdf: 92160 transmission: 46080 @@ -24,26 +23,41 @@ website_keywords: zoning: 11520 utility: 5760 electric: 5760 + franchise: 5760 + code: 2880 right-of-way: 1440 + easement: 1440 + siting: 1440 + franchise agreement: 1440 overhead: 720 + substation: 720 permit: 720 land use: 720 + planning: 720 + grantee: 720 municipal: 360 county: 360 code of ordinances: 360 ordinance code: 360 land use code: 360 + use table: 360 conditional use permit: 180 special use permit: 180 + chapter: 180 + article: 180 + title: 180 statute: 180 administrative code: 180 government: 180 -heuristic_keywords: true heuristic_keywords: good_tech_keywords: - "transmission" + - "kilovolt" - "powerline" + - "franchise" + - "grantee" + - "substation" good_tech_acronyms: - "kv" - "hvdc" From 252939826ac1b5be835c2cc285e40101b6537b70 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:23:34 -0600 Subject: [PATCH 051/103] Adding franchise as an output feature --- compass/extraction/transmission/transmission_schema.json5 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7ec8e76ea..c4b6a6512 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -93,6 +93,10 @@ "traffic disruption", // Vegetation Protection or Landscaping Requirements "vegetation", + + // -- Administrative -- + // Electric utility franchise granted to a named company + "franchise", ] }, "value": { From 2b8f7d15632f0088a66d6691f1a627f7390f23d2 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:26:19 -0600 Subject: [PATCH 052/103] doc, style: --- compass/plugin/one_shot/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index 9b639ed26..e4857a713 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -57,7 +57,7 @@ class _CacheKey(StrEnum): def _inject_scope_sentinels(schema): - """Prepend ``all`` and append ``other`` to the scope enum in-place""" + """Prepend ``all`` and append ``other`` to the scope enum""" try: scope = schema["properties"]["outputs"]["items"]["properties"][ "scope" From ba2c62b6f9451918283c8fe921555419c1096c90 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:52:08 -0600 Subject: [PATCH 053/103] feat: Apply sentinel if scope is used but undefined --- compass/plugin/one_shot/base.py | 8 +++----- compass/plugin/one_shot/components.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index e4857a713..18dc7d66b 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -28,6 +28,8 @@ SchemaBasedTextCollector, SchemaBasedTextExtractor, SchemaOrdinanceParser, + SCOPE_SENTINEL_ALL, + SCOPE_SENTINEL_OTHER, ) from compass.plugin.one_shot.cache import key_from_cache, key_to_cache from compass.services.threaded import CLEANED_FP_REGISTRY @@ -52,12 +54,8 @@ class _CacheKey(StrEnum): HEURISTIC_KEYWORDS = auto() -SCOPE_SENTINEL_ALL = "all" -SCOPE_SENTINEL_OTHER = "other" - - def _inject_scope_sentinels(schema): - """Prepend ``all`` and append ``other`` to the scope enum""" + """Add ``all`` and ``other`` sentinels to the scope enum in-place""" try: scope = schema["properties"]["outputs"]["items"]["properties"][ "scope" diff --git a/compass/plugin/one_shot/components.py b/compass/plugin/one_shot/components.py index 23b7dbdb4..4690f17d6 100644 --- a/compass/plugin/one_shot/components.py +++ b/compass/plugin/one_shot/components.py @@ -16,6 +16,10 @@ logger = logging.getLogger(__name__) + +SCOPE_SENTINEL_ALL = "all" +SCOPE_SENTINEL_OTHER = "other" + _DEFAULT_TEXT_SCOPE_SYSTEM_PROMPT = """\ You are a structured extraction scope validator. Given a text chunk, \ determine whether the chunk is within the given extraction scope. \ @@ -536,6 +540,7 @@ def _to_dataframe(self, data): {"feature": all_features, "quantitative": quant} ) full_df = full_df.merge(df, on="feature", how="left") + full_df = self._apply_scope_defaults(full_df) ignore_cols = { "county", @@ -552,3 +557,14 @@ def _to_dataframe(self, data): if col.name in full_df.columns and col.name not in ignore_cols ] return full_df[["feature", *out_cols, "quantitative"]] + + def _apply_scope_defaults(self, df): + """Fill placeholder scope entries with the 'all' sentinel""" + + output_items = self.SCHEMA["properties"]["outputs"]["items"] + if "scope" not in output_items.get("properties", {}): + return df + if "scope" not in df.columns: + df["scope"] = SCOPE_SENTINEL_ALL + df["scope"] = df["scope"].fillna(SCOPE_SENTINEL_ALL) + return df From 9dc637d87ddeb6584198c9c8b36683ba9e05d68d Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 15:55:46 -0600 Subject: [PATCH 054/103] Expanding non-technical words --- .../extraction/transmission/plugin_config.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 7ff4c50c4..193213b68 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -80,10 +80,22 @@ heuristic_keywords: - "500 kv" - "765 kv" not_tech_words: + - "local distribution" - "transmission fluid" - - "radio tower" + - "auto transmission" + - "automatic transmission" + - "communication transmission" + - "manual transmission" + - "data transmission" + - "radio transmission" + - "wireless transmission" + - "telephone line" + - "disease transmission" + - "virus transmission" + - "heat transmission" + - "natural gas transmission" - "gas transmission" - + - "oil transmission" collection_prompts: true From 43266a187f76d43ee2fd85b341cdabd3de87722a Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 16:02:38 -0600 Subject: [PATCH 055/103] test: _inject_scope_sentinels() --- .../unit/plugin/test_plugin_one_shot.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/python/unit/plugin/test_plugin_one_shot.py b/tests/python/unit/plugin/test_plugin_one_shot.py index 67610291c..c08c80a15 100644 --- a/tests/python/unit/plugin/test_plugin_one_shot.py +++ b/tests/python/unit/plugin/test_plugin_one_shot.py @@ -1,11 +1,15 @@ """COMPASS one-shot plugin tests""" +from copy import deepcopy from pathlib import Path import pytest from compass.exceptions import COMPASSPluginConfigurationError -from compass.plugin.one_shot.base import _normalize_heuristic_keywords +from compass.plugin.one_shot.base import ( + _inject_scope_sentinels, + _normalize_heuristic_keywords, +) from compass.plugin.one_shot.components import SchemaBasedTextCollector from compass.validation.content import ParseChunksWithMemory from compass.warn import COMPASSPluginConfigurationWarning @@ -219,6 +223,58 @@ async def test_schema_text_collector_stores_chunk_after_scope_and_context(): 1: "operative geothermal setback text", } +def _schema_with_scope(enum): + return { + "properties": { + "outputs": { + "items": { + "properties": {"scope": {"enum": list(enum)}} + } + } + } + } + +def test_inject_scope_sentinels_wraps_enum(): + """``all`` is prepended and ``other`` appended around user values""" + + schema = _schema_with_scope(["residential", "commercial"]) + _inject_scope_sentinels(schema) + + enum = schema["properties"]["outputs"]["items"]["properties"]["scope"][ + "enum" + ] + assert enum == ["all", "residential", "commercial", "other"] + + +def test_inject_scope_sentinels_is_idempotent(): + """Calling twice does not duplicate sentinels""" + + schema = _schema_with_scope(["residential"]) + _inject_scope_sentinels(schema) + _inject_scope_sentinels(schema) + + enum = schema["properties"]["outputs"]["items"]["properties"]["scope"][ + "enum" + ] + assert enum == ["all", "residential", "other"] + + +def test_inject_scope_sentinels_noop_when_no_scope_property(): + """Legacy schemas without a scope property are left unchanged""" + + schema = { + "properties": { + "outputs": { + "items": {"properties": {"feature": {"enum": ["a", "b"]}}} + } + } + } + original = deepcopy(schema) + + _inject_scope_sentinels(schema) + + assert schema == original + if __name__ == "__main__": pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) From ff3a9d0ada07e28b93a7a4923816572a13507976 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 16:07:43 -0600 Subject: [PATCH 056/103] feat: Adding property scope --- .../transmission/transmission_schema.json5 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index c4b6a6512..23c3b5eee 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -21,6 +21,19 @@ ], "additionalProperties": false, "properties": { + "scope": { + "type": "string", + "description": "The subset of the jurisdiction to which this row applies. Use 'all' when the requirement applies jurisdiction-wide with no zone-specific variation. Use one of the named zoning categories when the ordinance text explicitly ties the requirement to that district. Use 'other' when the ordinance names a specific district that does not match any of the enumerated categories (for example, 'PUD', 'aviation overlay', 'conservation-2'). Do not invent categories; do not infer a zone from headings or nearby context that does not explicitly bind the requirement to that district.", + "enum": [ + "rural", + "residential", + "commercial", + "light industrial", + "industrial", + "agricultural", + "mixed use" + ] + }, "feature": { "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", From 11ca1fbb743969cc1a02c3caf0d2f9bbe861c361 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 16:09:14 -0600 Subject: [PATCH 057/103] feat: Adjust value definition to use scope --- compass/extraction/transmission/transmission_schema.json5 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 23c3b5eee..5453c57c7 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -8,11 +8,12 @@ "properties": { "outputs": { "type": "array", - "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per feature**. Never infer, imply, or guess a requirement from related context.", + "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per (feature, scope) pair**. Emit one row per scope only when the ordinance text explicitly states different values or requirements per scope for the same feature; otherwise emit a single row with scope='all'. Never infer, imply, or guess a requirement from related context.", "items": { "type": "object", "required": [ "feature", + "scope", "value", "units", "section", From b04e3a8ad34b79672a629f851e56536a943ba6f9 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 16:19:35 -0600 Subject: [PATCH 058/103] feat: Franchise definition --- compass/extraction/transmission/transmission_schema.json5 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 5453c57c7..6e58735f7 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -241,6 +241,14 @@ } } }, + "administrative_features": { + "description": "Administrative or legal grants recorded in the ordinance code that do not impose a physical or numeric requirement on transmission infrastructure. Extract only when the ordinance text explicitly names the grant and its holder(s).", + "properties": { + "franchise": { + "description": "Electric utility franchise granted by this jurisdiction to a named company or companies. Extract only when the ordinance explicitly grants (or renews, extends, or amends) an electric franchise, right, or privilege to operate electric infrastructure in the jurisdiction. In summary, name every explicitly named grantee company using its verbatim legal name and quote the granting clause. Do not extract water, gas, cable, telecom, or other non-electric franchises. Do not infer a franchise from operational references to a utility elsewhere in the code. Emit at most one row per jurisdiction; if multiple electric franchises are named, list all grantees in the same summary." + } + } + } }, "$examples": [ { From 645364bfeb4e7b4e66a3820c148985e01c93c2b4 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 16:33:09 -0600 Subject: [PATCH 059/103] feat: Examples of extracted scope --- .../transmission/transmission_schema.json5 | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 6e58735f7..df06572a9 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -253,11 +253,39 @@ "$examples": [ { "feature": "noise", + "scope": "all", "value": 50, "units": "dBA", "section": "SECTION 1308 – Performance / Construction Standards", - // Need some editing - "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance." + "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance.", + "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single record with scope='all' is used." + }, + { + "feature": "noise", + "scope": "residential", + "value": 45, + "units": "dBA", + "section": "Table 5-3 – Noise Standards by District", + "summary": "Maximum permissible operational noise at the property line for transmission facilities in residential districts is 45 dBA.", + "explanation": "Ordinance explicitly states a distinct noise limit for residential districts. Other districts are also defined." + }, + { + "feature": "noise", + "scope": "commercial", + "value": 55, + "units": "dBA", + "section": "Table 5-3 – Noise Standards by District", + "summary": "Maximum permissible operational noise at the property line for transmission facilities in commercial districts is 55 dBA.", + "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other disctricts are also defined." + }, + { + "feature": "zone use restriction", + "scope": "residential", + "value": null, + "units": null, + "section": "Sec. 12.04.A – Prohibited Uses in Residential Districts", + "summary": "Overhead electric transmission lines rated above 69 kV are prohibited in all residential districts (R-1, R-2, R-3).", + "explanation": "Ordinance explicitly bans transmission lines from residential districts; scope='residential' captures the district class." } ], "$instructions": { From 96379b46891d7d80e49171bd9a42af6b23148272 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 15 Jul 2026 22:13:22 -0600 Subject: [PATCH 060/103] style: --- compass/plugin/one_shot/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index 18dc7d66b..efaa6b7ba 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -57,9 +57,7 @@ class _CacheKey(StrEnum): def _inject_scope_sentinels(schema): """Add ``all`` and ``other`` sentinels to the scope enum in-place""" try: - scope = schema["properties"]["outputs"]["items"]["properties"][ - "scope" - ] + scope = schema["properties"]["outputs"]["items"]["properties"]["scope"] except (KeyError, TypeError): return From 96f88720680c871519812c4abe043c3e3dc13ada Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 12:15:52 -0600 Subject: [PATCH 061/103] Adding explicit CPCN as keyword --- compass/extraction/transmission/plugin_config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 193213b68..97fbaa65f 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -65,6 +65,7 @@ heuristic_keywords: - "uhv" - "cpcn" # Certificate of Public Convenience and Necessity good_tech_phrases: + - "certificate of public convenience and necessity" - "high voltage" - "high-voltage" - "extra high voltage" From dc376232713ace7767c3e0830a3c6c623f5227fe Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 12:30:38 -0600 Subject: [PATCH 062/103] feat: Missing maximum height and electric field --- .../transmission/transmission_schema.json5 | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index df06572a9..7566b0c10 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -60,8 +60,12 @@ // -- Numerical Restrictions -- // Electric Fields Strength at Property Line "electric field", - // Tower vs facility height. Do I want both? - "maximum height", + // Maximum allowed height for transmission towers, poles, + // or monopoles that carry high-voltage conductors. + "maximum tower height", + // Maximum allowed height for accessory or support + // structures (substations, control buildings, etc.). + "maximum structure height", "noise", // -- Operational -- @@ -196,6 +200,12 @@ "numerical_features": { "description": "Non-setback numerical restriction features. Only extract if numerical values are explicitly given in the text.", "properties": { + "electric field": { + "description": "Extract maximum electric field strength allowed at a property line." + }, + "maximum height": { + "description": "Extract maximum structure height allowed. If the ordinance includes separate maximum height limits for towers and other support structures, extract them as separate rows under 'maximum tower height' and 'maximum structure height' features respectively, even if the numeric values are the same." + }, "noise": { "description": "Extract maximum allowable operational noise for electric transmission lines and related facilities only when an explicit numeric limit is stated. Normalize A-weighted units to 'dBA' in units and preserve verbatim wording in summary. If the ordinance only references compliance with external standards or provides no numeric noise limit, omit this feature entirely." }, From 77a4c3b8bb84ada6cb015b86e0e9cc1917daaf89 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 12:35:16 -0600 Subject: [PATCH 063/103] Split maximum height into tower and structures --- compass/extraction/transmission/transmission_schema.json5 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7566b0c10..f68e734e2 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -203,8 +203,11 @@ "electric field": { "description": "Extract maximum electric field strength allowed at a property line." }, - "maximum height": { - "description": "Extract maximum structure height allowed. If the ordinance includes separate maximum height limits for towers and other support structures, extract them as separate rows under 'maximum tower height' and 'maximum structure height' features respectively, even if the numeric values are the same." + "maximum tower height": { + "description": "Extract the maximum allowed height for transmission towers, poles, monopoles, or other vertical structures that carry high-voltage conductors. Do not conflate with accessory structures such as substations, switchyards, control buildings, or equipment enclosures; those belong under 'maximum structure height'. If the ordinance states a single maximum height that applies to all transmission-related vertical elements without distinguishing towers from accessory structures, emit only 'maximum tower height'." + }, + "maximum structure height": { + "description": "Extract the maximum allowed height for accessory or support structures related to transmission infrastructure that do not carry conductors, including substations, switchyards, control buildings, equipment enclosures, and similar auxiliary facilities. Do not conflate with the towers, poles, or monopoles carrying the conductors; those belong under 'maximum tower height'. Extract only when the ordinance explicitly states a height limit for such accessory structures separate from the tower limit." }, "noise": { "description": "Extract maximum allowable operational noise for electric transmission lines and related facilities only when an explicit numeric limit is stated. Normalize A-weighted units to 'dBA' in units and preserve verbatim wording in summary. If the ordinance only references compliance with external standards or provides no numeric noise limit, omit this feature entirely." From 9c8fc52faffc529d7eb5e7c6cd3a0cc2dde91470 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 12:37:57 -0600 Subject: [PATCH 064/103] Defining scope --- .../extraction/transmission/transmission_schema.json5 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index f68e734e2..6ae4e05ea 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -310,7 +310,15 @@ "Prefer numeric values with units ('feet', 'meters').", "Setback rows must contain numeric value and non-null units; never emit qualitative-only setback rows.", "Treat property-line, lot-line, and parcel-boundary setbacks as 'property lines' when the ordinance measures the setback from that boundary.", - ] + ], + "scope": [ + "Set scope='all' when the ordinance states a single requirement that applies jurisdiction-wide with no zone-specific variation.", + "Set scope to a named zoning category only when the ordinance text explicitly binds the requirement to that district (for example a table of limits per district, or a clause 'in residential districts, ...').", + "Set scope='other' when the ordinance names a specific district that does not match any enumerated category (for example PUD, aviation overlay, conservation-2).", + "Do not invent scopes from section headings, chapter titles, or nearby context that does not explicitly bind the requirement to that district.", + "When the same feature has different requirements in different districts, emit one row per district; do not merge them into a single row.", + "For the 'zone use restriction' feature, scope must be a specific district (never 'all'); emit one row per explicitly restricted district.", + ], }, "$qualitative_features": [ "decommissioning", From c3c1ba1d41c9f30a3aa68f0f4ad66b50ddb435e8 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 12:58:51 -0600 Subject: [PATCH 065/103] Transitioning to zone restriction features Instead of multiple features, a single one for different scopes. --- .../transmission/transmission_schema.json5 | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 6ae4e05ea..c1008355e 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -78,14 +78,12 @@ // Compliance with Federal Aviation Administration and local approvals near airports "FAA", - // -- Prohibited or limited-use districs -- - // Maybe call these zones, such as rural zone? - "rural zone", - "residential zone", - "commercial zone", - "light industrial zone", - // Includes irrigated farmland, ranch land. - "agricultural zone", + // -- Prohibited or limited-use districts -- + // Use the `scope` field to identify which district the + // prohibition applies to (rural, residential, commercial, + // light industrial, industrial, agricultural, mixed use, + // or 'other' for named districts not covered by the enum). + "zone use restriction", // -- Exclusions -- "historic site", "critical species habitat", @@ -235,7 +233,7 @@ } }, "prohibition_features": { - "description": "Prohibitions, bans, or moratoria on building, installing, siting, or otherwise deploying electric transmission lines and related infrastructure in certain areas or under certain conditions. These features require text extraction only.", + "description": "Prohibitions, bans, moratoria, or explicit siting restrictions on building, installing, or otherwise deploying electric transmission lines and related infrastructure within a specific zoning district. Use the `scope` field to identify the district. Extract only when the ordinance explicitly states that the restriction applies to transmission lines or related infrastructure. These features require text extraction only.", "properties": { "rural zone": { "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in rural zones, agricultural zones, or similar low-density land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." From e2cd06d7308005b44468be2cd75bc093272b3ff3 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 21:18:55 -0600 Subject: [PATCH 066/103] List of qualitative features --- .../extraction/transmission/transmission_schema.json5 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index c1008355e..835926ac1 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -319,6 +319,17 @@ ], }, "$qualitative_features": [ + "bond", + "color", "decommissioning", + "fencing", + "fire prevention", + "franchise", + "lightning", + "permit", + "signage", + "traffic disruption", + "vegetation", + "FAA" ] } From 99eb96534d5908e57641c182b8d728ae234a5b51 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 21:22:00 -0600 Subject: [PATCH 067/103] Updating description of units --- compass/extraction/transmission/transmission_schema.json5 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 835926ac1..6c8ee059b 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -130,7 +130,8 @@ }, "units": { "type": ["string", "null"], - "description": "Units for the extracted value. Preserve the ordinance unit wording exactly whenever possible. For setbacks/depth, use linear units such as 'feet' or 'meters' as stated in text. For noise, use 'dBA' only if the ordinance says 'dBA' or 'dB(A)'; if it says 'dB' without A-weighting, keep 'dB'. Use null for qualitative fields without measurable units." + "description": "Units for the extracted value. Use canonical units for interoperability. For distance and height features, use 'feet' or 'meters' when possible. For noise, normalize A-weighted variants such as 'dB(A)' or 'dBA' to 'dBA'; keep plain 'dB' only when the text is explicitly not A-weighted. Preserve verbatim ordinance wording in summary while keeping units standardized in this field. Use null for permit lists and qualitative requirements without measurable units." + }, "section": { "type": ["string", "null"], From 9c3be38d55fe0faefa11ba04d482c7a26a7c9ab5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 21:23:29 -0600 Subject: [PATCH 068/103] Updating value description --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 6c8ee059b..59f0d7179 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -116,7 +116,7 @@ ] }, "value": { - "description": "The extracted ordinance value. For numerical setbacks/limits, use a number. For categorical outcomes, use a string. For fields that list multiple named items, use an array of strings. Use null only for qualitative features, and only when an enacted, explicit, enforceable ordinance requirement for that feature is present. Null must never be used to indicate absence. If a feature has no enacted, explicit requirement in the ordinance text, omit that feature from outputs.", + "description": "The extracted ordinance value. For numerical distance thresholds and limits, use a number. For categorical outcomes, use a string. For fields that list multiple named items, use an array of strings. Use null only for qualitative features, and only when an enacted, explicit, enforceable ordinance requirement for that feature is present. Null must never be used to indicate absence. If a feature has no enacted, explicit requirement in the ordinance text, omit that feature from outputs.", "anyOf": [ {"type": "number"}, {"type": "string"}, From a176ba50cc0af38c8b9730e44867490d15534b2e Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 21:26:31 -0600 Subject: [PATCH 069/103] Refactoring restricted use and adding exclusions Restricted use is now a single feature that takes advantage of the jurisdiction x zone/scope pair. --- .../transmission/transmission_schema.json5 | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 59f0d7179..6797f5546 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -236,20 +236,40 @@ "prohibition_features": { "description": "Prohibitions, bans, moratoria, or explicit siting restrictions on building, installing, or otherwise deploying electric transmission lines and related infrastructure within a specific zoning district. Use the `scope` field to identify the district. Extract only when the ordinance explicitly states that the restriction applies to transmission lines or related infrastructure. These features require text extraction only.", "properties": { - "rural zone": { - "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in rural zones, agricultural zones, or similar low-density land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + "zone use restriction": { + "description": "Extract prohibitions, bans, moratoria, or explicit siting restrictions on electric transmission lines and related infrastructure within a specific zoning district. Set `scope` to the district being restricted (for example 'residential', 'commercial', 'agricultural'). Use scope='other' when the ordinance names a district that does not match the enumerated categories. Do not emit scope='all' for this feature; if the restriction is jurisdiction-wide, it is not a zone use restriction and should be captured under a different feature. Emit one row per explicitly restricted district. Include the verbatim ordinance language in summary and cite the restricting clause." + } + } + }, + "excluded_lands": { + "description": "Lands excluded from transmission line siting. Do not extract as prohibitions or setbacks. Only extract when the ordinance explicitly states that transmission lines and related infrastructure are prohibited from being sited, built, or installed in these areas.", + "properties": { + "historic site": { + "description": "Extract exclusions for historic sites, landmarks, or districts." + }, + "critical species habitat": { + "description": "Extract exclusions for critical species habitat, protected wildlife areas, or similar conservation lands." + }, + "wetland": { + "description": "Extract exclusions for wetlands, marshes, swamps, or similar aquatic habitats." + }, + "floodplain": { + "description": "Extract exclusions for floodplains, flood zones, or similar areas prone to flooding." + }, + "residence": { + "description": "Extract exclusions for residences, homes, occupied dwellings, or similar occupied buildings." }, - "residential zone": { - "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in residential zones, residential districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + "hospital": { + "description": "Extract exclusions for hospitals, medical centers, healthcare facilities, or similar critical community facilities." }, - "commercial zone": { - "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in commercial zones, commercial districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + "school": { + "description": "Extract exclusions for schools, educational institutions, daycare centers, institutions of higher education, or similar educational facilities." }, - "light industrial zone": { - "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in light industrial zones, light industrial districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure." + "daycare": { + "description": "Extract exclusions for daycare centers, childcare facilities, or similar child-focused facilities." }, - "agricultural zone": { - "description": "Extract prohibitions or moratoria on electric transmission lines and related infrastructure in agricultural zones, agricultural districts, or similar land use areas when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure. Include prohibitions in irrigated farmland, ranch land, or similar agricultural land when the ordinance explicitly states that the prohibition applies to transmission lines or related infrastructure in those areas." + "higher education": { + "description": "Extract exclusions for institutions of higher education, colleges, universities, or similar post-secondary educational facilities." } } }, From 06bafa8e075251d94f6f8bcbd5b602016440067b Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 16 Jul 2026 21:56:21 -0600 Subject: [PATCH 070/103] refact: Migrating from scope to subarea Scope for zone was a good choice, but scope was already used in some parts of the code in another context. To avoid confusion, let's use subarea instead. Note that I'm not sure this commit capture all the required renaming. I might need to review it again. --- compass/plugin/one_shot/base.py | 24 +++++++------ compass/plugin/one_shot/components.py | 19 +++++----- compass/utilities/finalize.py | 2 +- .../unit/plugin/test_plugin_one_shot.py | 35 ++++++++++--------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index efaa6b7ba..c5f61e51f 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -28,8 +28,8 @@ SchemaBasedTextCollector, SchemaBasedTextExtractor, SchemaOrdinanceParser, - SCOPE_SENTINEL_ALL, - SCOPE_SENTINEL_OTHER, + SUBAREA_SENTINEL_ALL, + SUBAREA_SENTINEL_OTHER, ) from compass.plugin.one_shot.cache import key_from_cache, key_to_cache from compass.services.threaded import CLEANED_FP_REGISTRY @@ -54,21 +54,23 @@ class _CacheKey(StrEnum): HEURISTIC_KEYWORDS = auto() -def _inject_scope_sentinels(schema): - """Add ``all`` and ``other`` sentinels to the scope enum in-place""" +def _inject_subarea_sentinels(schema): + """Add ``all``/``other`` sentinels to the subarea enum in-place""" try: - scope = schema["properties"]["outputs"]["items"]["properties"]["scope"] + subarea = schema["properties"]["outputs"]["items"]["properties"][ + "subarea" + ] except (KeyError, TypeError): return - enum = scope.get("enum") + enum = subarea.get("enum") if not isinstance(enum, list): return - if SCOPE_SENTINEL_ALL not in enum: - enum.insert(0, SCOPE_SENTINEL_ALL) - if SCOPE_SENTINEL_OTHER not in enum: - enum.append(SCOPE_SENTINEL_OTHER) + if SUBAREA_SENTINEL_ALL not in enum: + enum.insert(0, SUBAREA_SENTINEL_ALL) + if SUBAREA_SENTINEL_OTHER not in enum: + enum.append(SUBAREA_SENTINEL_OTHER) def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore[complex-structure] @@ -212,7 +214,7 @@ def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore if isinstance(config["schema"], str): config["schema"] = load_config(config["schema"]) - _inject_scope_sentinels(config["schema"]) + _inject_subarea_sentinels(config["schema"]) config["qual_feats"] = { f.casefold() for f in config["schema"].pop("$qualitative_features", []) diff --git a/compass/plugin/one_shot/components.py b/compass/plugin/one_shot/components.py index 4690f17d6..963926426 100644 --- a/compass/plugin/one_shot/components.py +++ b/compass/plugin/one_shot/components.py @@ -17,8 +17,8 @@ logger = logging.getLogger(__name__) -SCOPE_SENTINEL_ALL = "all" -SCOPE_SENTINEL_OTHER = "other" +SUBAREA_SENTINEL_ALL = "all" +SUBAREA_SENTINEL_OTHER = "other" _DEFAULT_TEXT_SCOPE_SYSTEM_PROMPT = """\ You are a structured extraction scope validator. Given a text chunk, \ @@ -540,7 +540,7 @@ def _to_dataframe(self, data): {"feature": all_features, "quantitative": quant} ) full_df = full_df.merge(df, on="feature", how="left") - full_df = self._apply_scope_defaults(full_df) + full_df = self._apply_subarea_defaults(full_df) ignore_cols = { "county", @@ -558,13 +558,10 @@ def _to_dataframe(self, data): ] return full_df[["feature", *out_cols, "quantitative"]] - def _apply_scope_defaults(self, df): - """Fill placeholder scope entries with the 'all' sentinel""" + def _apply_subarea_defaults(self, df): + """Fill placeholder subarea entries with the 'all' sentinel""" - output_items = self.SCHEMA["properties"]["outputs"]["items"] - if "scope" not in output_items.get("properties", {}): - return df - if "scope" not in df.columns: - df["scope"] = SCOPE_SENTINEL_ALL - df["scope"] = df["scope"].fillna(SCOPE_SENTINEL_ALL) + if "subarea" not in df.columns: + df["subarea"] = SUBAREA_SENTINEL_ALL + df["subarea"] = df["subarea"].fillna(SUBAREA_SENTINEL_ALL) return df diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index 12f449192..00f14a52c 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -24,7 +24,7 @@ "jurisdiction_type", "FIPS", "feature", - "scope", + "subarea", "value", "units", "adder", diff --git a/tests/python/unit/plugin/test_plugin_one_shot.py b/tests/python/unit/plugin/test_plugin_one_shot.py index c08c80a15..b2e9c3ce2 100644 --- a/tests/python/unit/plugin/test_plugin_one_shot.py +++ b/tests/python/unit/plugin/test_plugin_one_shot.py @@ -228,39 +228,40 @@ def _schema_with_scope(enum): "properties": { "outputs": { "items": { - "properties": {"scope": {"enum": list(enum)}} + "properties": {"subarea": {"enum": list(enum)}} } } } } -def test_inject_scope_sentinels_wraps_enum(): + +def test_inject_subarea_sentinels_wraps_enum(): """``all`` is prepended and ``other`` appended around user values""" - schema = _schema_with_scope(["residential", "commercial"]) - _inject_scope_sentinels(schema) + schema = _schema_with_subarea(["residential", "commercial"]) + _inject_subarea_sentinels(schema) - enum = schema["properties"]["outputs"]["items"]["properties"]["scope"][ - "enum" - ] + enum = schema["properties"]["outputs"]["items"]["properties"][ + "subarea" + ]["enum"] assert enum == ["all", "residential", "commercial", "other"] -def test_inject_scope_sentinels_is_idempotent(): +def test_inject_subarea_sentinels_is_idempotent(): """Calling twice does not duplicate sentinels""" - schema = _schema_with_scope(["residential"]) - _inject_scope_sentinels(schema) - _inject_scope_sentinels(schema) + schema = _schema_with_subarea(["residential"]) + _inject_subarea_sentinels(schema) + _inject_subarea_sentinels(schema) - enum = schema["properties"]["outputs"]["items"]["properties"]["scope"][ - "enum" - ] + enum = schema["properties"]["outputs"]["items"]["properties"][ + "subarea" + ]["enum"] assert enum == ["all", "residential", "other"] -def test_inject_scope_sentinels_noop_when_no_scope_property(): - """Legacy schemas without a scope property are left unchanged""" +def test_inject_subarea_sentinels_noop_when_no_subarea_property(): + """Legacy schemas without a subarea property are left unchanged""" schema = { "properties": { @@ -271,7 +272,7 @@ def test_inject_scope_sentinels_noop_when_no_scope_property(): } original = deepcopy(schema) - _inject_scope_sentinels(schema) + _inject_subarea_sentinels(schema) assert schema == original From a1a5e38abf4dd108cd3b9986662c176561ee75c5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 12:15:21 -0600 Subject: [PATCH 071/103] typo: --- compass/extraction/transmission/transmission_schema.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 6797f5546..7d9a334be 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -45,7 +45,7 @@ // Occupied Dwellings or Residential Buildings "residential buildings", "property lines", - // Roads of frewways + // Roads of freeways "roads", // Heliports and Airport Runways "airport", @@ -139,7 +139,7 @@ }, "summary": { "type": "string", - "description": "A short summary of the relevant ordinance requiremets." + "description": "A short summary of the relevant ordinance requirements." }, "explanation": { "type": "string", From 0487cec52eecaabbdb9027efed155eb63cc10c9a Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 12:20:26 -0600 Subject: [PATCH 072/103] fix: Wrong function name from renaming Current name is _inject_subarea_sentinels(). --- tests/python/unit/plugin/test_plugin_one_shot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/unit/plugin/test_plugin_one_shot.py b/tests/python/unit/plugin/test_plugin_one_shot.py index b2e9c3ce2..ace356eeb 100644 --- a/tests/python/unit/plugin/test_plugin_one_shot.py +++ b/tests/python/unit/plugin/test_plugin_one_shot.py @@ -7,7 +7,7 @@ from compass.exceptions import COMPASSPluginConfigurationError from compass.plugin.one_shot.base import ( - _inject_scope_sentinels, + _inject_subarea_sentinels, _normalize_heuristic_keywords, ) from compass.plugin.one_shot.components import SchemaBasedTextCollector From e7085f2abba1f2efc86d430ef31de45f434bac66 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 12:39:23 -0600 Subject: [PATCH 073/103] fix: Forgot to update name When moving to subarea, left behind this _schema_with_scope. --- tests/python/unit/plugin/test_plugin_one_shot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/unit/plugin/test_plugin_one_shot.py b/tests/python/unit/plugin/test_plugin_one_shot.py index ace356eeb..00af501e5 100644 --- a/tests/python/unit/plugin/test_plugin_one_shot.py +++ b/tests/python/unit/plugin/test_plugin_one_shot.py @@ -223,7 +223,7 @@ async def test_schema_text_collector_stores_chunk_after_scope_and_context(): 1: "operative geothermal setback text", } -def _schema_with_scope(enum): +def _schema_with_subarea(enum): return { "properties": { "outputs": { From da84cb6a636d4099a7598c243c6df6fea0d3a7bf Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 17:55:53 -0600 Subject: [PATCH 074/103] refact: Adding 'distance' to setback features Following the pattern used in GHP. --- .../transmission/transmission_schema.json5 | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7d9a334be..a4145e89a 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -41,17 +41,17 @@ "enum": [ // -- Setbacks -- // Do I need to include participating and non-participating - "structures", + "structures distance", // Occupied Dwellings or Residential Buildings - "residential buildings", - "property lines", + "residential buildings distance", + "property lines distance", // Roads of freeways - "roads", + "roads distance", // Heliports and Airport Runways - "airport", + "airport distance", // Ordinary High Water Mark (OHWM) - "OHWM", - "railroads", + "OHWM distance", + "railroads distance", // -- Definitions -- // Definition /voltage threshold to qualify as a "transmission line" @@ -173,25 +173,25 @@ "setback_features": { "description": "Setback features for electrical transmission lines and related infrastructure. Treat each setback feature independently and do not cross-apply a setback unless the ordinance text explicitly states that it applies to multiple target types. When a single clause explicitly lists multiple target types and one shared numeric setback, emit one row per explicitly listed feature using the same numeric value and units and cite the same clause in summary. Apply the shared numeric prioritization rules in $core_principles when multiple numeric values explicitly apply to the same feature.", "properties": { - "structures": { - "description": "Minimum required separation from structures and buildings that do not match the definition of 'residential buildings'." + "structures distance": { + "description": "Minimum required separation from structures and buildings that do not match the definition of 'residential buildings distance'." }, - "residential buildings": { + "residential buildings distance": { "description": "Minimum required separation from residential buildings, occupied dwellings, occupied buildings, residences, homes, residential receptors, or residential uses." }, - "property lines": { + "property lines distance": { "description": "Minimum required separation from property lines, lot lines, parcel boundaries, or lease boundaries when the ordinance explicitly states the distance is measured from that boundary. Do not remap property-line distances to roads or residential zones unless the text explicitly makes them equivalent for that requirement. Distances to official plan lines or specific plan lines for public highways do not belong here unless the ordinance expressly defines those lines as property boundaries for the same requirement." }, - "roads": { - "description": "Minimum required separation from public road rights-of-way. Property-line setbacks do NOT count for this feature unless the ordinance text explicitly states that the property line is the road right-of-way or otherwise makes them the same boundary for that requirement. IGNORE: Do not respond based on setbacks from other kinds of right-of-way such as utility easements, etc." + "roads distance": { + "description": "Minimum required separation from public road rights-of-way, including freeways. Property-line setbacks do NOT count for this feature unless the ordinance text explicitly states that the property line is the road right-of-way or otherwise makes them the same boundary for that requirement. IGNORE: Do not respond based on setbacks from other kinds of right-of-way such as utility easements, etc." }, - "airport": { + "airport distance": { "description":"Minimum required separation from airport runways and heliports." }, - "OHWM": { + "OHWM distance": { "description":"Minimum required separation from Ordinary High Water Mark (OHWM)." }, - "railroads": { + "railroads distance": { "description": "Minimum required separation from railroads, railroad rights-of-way, rail corridors, or active rail lines. Extract only when rail infrastructure is explicitly named." } } From 6903d45527c404360bf1ff735c125e2ee3da2e79 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 17:59:32 -0600 Subject: [PATCH 075/103] refact: Renaming scope to subarea We already use scope in another context, so let's avoid confusion here. --- .../transmission/transmission_schema.json5 | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index a4145e89a..51aeb88dd 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -13,7 +13,7 @@ "type": "object", "required": [ "feature", - "scope", + "subarea", "value", "units", "section", @@ -22,19 +22,6 @@ ], "additionalProperties": false, "properties": { - "scope": { - "type": "string", - "description": "The subset of the jurisdiction to which this row applies. Use 'all' when the requirement applies jurisdiction-wide with no zone-specific variation. Use one of the named zoning categories when the ordinance text explicitly ties the requirement to that district. Use 'other' when the ordinance names a specific district that does not match any of the enumerated categories (for example, 'PUD', 'aviation overlay', 'conservation-2'). Do not invent categories; do not infer a zone from headings or nearby context that does not explicitly bind the requirement to that district.", - "enum": [ - "rural", - "residential", - "commercial", - "light industrial", - "industrial", - "agricultural", - "mixed use" - ] - }, "feature": { "type": "string", "description": "The ordinance feature being extracted. Must be one of the enumerated feature IDs. Do not invent aliases, prefixes, or synonym variants.", @@ -79,7 +66,7 @@ "FAA", // -- Prohibited or limited-use districts -- - // Use the `scope` field to identify which district the + // Use the `subarea` field to identify which district the // prohibition applies to (rural, residential, commercial, // light industrial, industrial, agricultural, mixed use, // or 'other' for named districts not covered by the enum). @@ -115,6 +102,19 @@ "franchise", ] }, + "subarea": { + "type": "string", + "description": "The subset of the jurisdiction to which this row applies. Use 'all' when the requirement applies jurisdiction-wide with no zone-specific variation. Use one of the named zoning categories when the ordinance text explicitly ties the requirement to that district. Use 'other' when the ordinance names a specific district that does not match any of the enumerated categories (for example, 'PUD', 'aviation overlay', 'conservation-2'). Do not invent categories; do not infer a zone from headings or nearby context that does not explicitly bind the requirement to that district.", + "enum": [ + "rural", + "residential", + "commercial", + "light industrial", + "industrial", + "agricultural", + "mixed use" + ] + }, "value": { "description": "The extracted ordinance value. For numerical distance thresholds and limits, use a number. For categorical outcomes, use a string. For fields that list multiple named items, use an array of strings. Use null only for qualitative features, and only when an enacted, explicit, enforceable ordinance requirement for that feature is present. Null must never be used to indicate absence. If a feature has no enacted, explicit requirement in the ordinance text, omit that feature from outputs.", "anyOf": [ @@ -161,8 +161,7 @@ "description": "Emit only positively matched features. If a feature is not explicitly present, omit it entirely rather than returning placeholder text. For qualitative features, use value=null and units=null only when an enacted, explicit requirement or definition for that same feature is present. For numeric features, extract only when an explicit numeric threshold is stated in the ordinance text; otherwise omit the feature instead of returning null, empty, or qualitative-only values. Never emit absence placeholders such as 'not found', 'no explicit requirement', 'none', or similar text in any field." }, "numeric_prioritization": { - // Review numeric features if I specify all conditions here. - "description": "When multiple numeric values apply to the same feature, keep one row and select the controlling most restrictive value for that feature. Restrictiveness rules: setbacks -> choose the largest minimum separation distance; tower height -> choose the highest maximum height; noise -> choose the lowest allowed noise limit. Keep condition-specific alternatives in summary only when the ordinance text explicitly shows they all apply to the same feature for GHP systems." + "description": "When multiple numeric values apply to the same (feature, subarea) pair, keep one row for that pair and select the controlling most restrictive value. Restrictiveness rules: setbacks -> choose the largest minimum separation distance; maximum tower height and maximum structure height -> choose the lowest limit within each feature independently, never combine tower limits with accessory-structure limits; noise -> choose the lowest allowed noise limit. When the ordinance explicitly states different numeric limits per zoning district for the same feature (for example a noise table with distinct limits for residential vs commercial districts), emit one row per district using the appropriate `subarea` value and apply restrictiveness within each row. Keep condition-specific alternatives in summary only when the ordinance text explicitly shows they all apply to the same (feature, subarea) pair for electric transmission systems." }, /* "definition_v_rule_test": { @@ -285,25 +284,25 @@ "$examples": [ { "feature": "noise", - "scope": "all", + "subarea": "all", "value": 50, "units": "dBA", "section": "SECTION 1308 – Performance / Construction Standards", "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance.", - "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single record with scope='all' is used." + "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single row with subarea='all' is emitted." }, { "feature": "noise", - "scope": "residential", + "subarea": "residential", "value": 45, "units": "dBA", "section": "Table 5-3 – Noise Standards by District", "summary": "Maximum permissible operational noise at the property line for transmission facilities in residential districts is 45 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for residential districts. Other districts are also defined." + "explanation": "Ordinance explicitly states a distinct noise limit for residential districts; a companion row with subarea='commercial' captures the 55 dBA limit for commercial districts." }, { "feature": "noise", - "scope": "commercial", + "subarea": "commercial", "value": 55, "units": "dBA", "section": "Table 5-3 – Noise Standards by District", @@ -312,12 +311,12 @@ }, { "feature": "zone use restriction", - "scope": "residential", + "subarea": "residential", "value": null, "units": null, "section": "Sec. 12.04.A – Prohibited Uses in Residential Districts", "summary": "Overhead electric transmission lines rated above 69 kV are prohibited in all residential districts (R-1, R-2, R-3).", - "explanation": "Ordinance explicitly bans transmission lines from residential districts; scope='residential' captures the district class." + "explanation": "Ordinance explicitly bans transmission lines from residential districts; subarea='residential' captures the district class." } ], "$instructions": { @@ -330,11 +329,11 @@ "Setback rows must contain numeric value and non-null units; never emit qualitative-only setback rows.", "Treat property-line, lot-line, and parcel-boundary setbacks as 'property lines' when the ordinance measures the setback from that boundary.", ], - "scope": [ - "Set scope='all' when the ordinance states a single requirement that applies jurisdiction-wide with no zone-specific variation.", - "Set scope to a named zoning category only when the ordinance text explicitly binds the requirement to that district (for example a table of limits per district, or a clause 'in residential districts, ...').", - "Set scope='other' when the ordinance names a specific district that does not match any enumerated category (for example PUD, aviation overlay, conservation-2).", - "Do not invent scopes from section headings, chapter titles, or nearby context that does not explicitly bind the requirement to that district.", + "subarea": [ + "Set subarea='all' when the ordinance states a single requirement that applies jurisdiction-wide with no zone-specific variation.", + "Set subarea to a named zoning category only when the ordinance text explicitly binds the requirement to that district (for example a table of limits per district, or a clause 'in residential districts, ...').", + "Set subarea='other' when the ordinance names a specific district that does not match any enumerated category (for example PUD, aviation overlay, conservation-2).", + "Do not invent subareas from section headings, chapter titles, or nearby context that does not explicitly bind the requirement to that district.", "When the same feature has different requirements in different districts, emit one row per district; do not merge them into a single row.", "For the 'zone use restriction' feature, scope must be a specific district (never 'all'); emit one row per explicitly restricted district.", ], From 5cd2a6092fa7803f4dee4ee36de853ba208a807d Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sat, 18 Jul 2026 18:00:28 -0600 Subject: [PATCH 076/103] cfg: MVP schema as 0.1.0 --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 51aeb88dd..64ab59222 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -1,7 +1,7 @@ { "title": "Transmission Ordinance Extraction Schema", "description": "Single-shot structured extraction schema for energy transmission ordinances. This schema guides an LLM to extract all relevant features in one call and returns an outputs array where each object represents one row in the extracted long-form table.", - "version": "0.0.2", + "version": "0.1.0", "type": "object", "required": ["outputs"], "additionalProperties": false, From 9500d79fb136fd070bdc4b680ff861e3196ac504 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 09:18:23 -0600 Subject: [PATCH 077/103] typo: --- compass/extraction/transmission/transmission_schema.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 64ab59222..bde8c6d7e 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -307,7 +307,7 @@ "units": "dBA", "section": "Table 5-3 – Noise Standards by District", "summary": "Maximum permissible operational noise at the property line for transmission facilities in commercial districts is 55 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other disctricts are also defined." + "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other districts are also defined." }, { "feature": "zone use restriction", @@ -345,7 +345,7 @@ "fencing", "fire prevention", "franchise", - "lightning", + "lighting", "permit", "signage", "traffic disruption", From b8e81ab5a8792e2d13b5223777202eed242d0597 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 10:53:51 -0600 Subject: [PATCH 078/103] Split jurisdiction from query context --- compass/extraction/transmission/plugin_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 97fbaa65f..13e7aa4e6 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -14,7 +14,7 @@ query_templates: - "{jurisdiction}, power line" - "{jurisdiction}, electric utility franchise ordinance" - "{jurisdiction}, code of ordinances electric franchise" - - "filetype:pdf {jurisdiction} electric franchise agreement" + - "filetype:pdf {jurisdiction}, electric franchise agreement" website_keywords: pdf: 92160 From d17b4289253f34c9a1b551785e16a70a663d75e5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 11:00:41 -0600 Subject: [PATCH 079/103] refact: Aligning with other techs Ordering good_tech_acronyms definition to match previous techs. --- compass/extraction/transmission/plugin_config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 13e7aa4e6..868b38310 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -51,6 +51,12 @@ website_keywords: government: 180 heuristic_keywords: + good_tech_acronyms: + - "kv" + - "hvdc" + - "ehv" + - "uhv" + - "cpcn" # Certificate of Public Convenience and Necessity good_tech_keywords: - "transmission" - "kilovolt" @@ -58,12 +64,6 @@ heuristic_keywords: - "franchise" - "grantee" - "substation" - good_tech_acronyms: - - "kv" - - "hvdc" - - "ehv" - - "uhv" - - "cpcn" # Certificate of Public Convenience and Necessity good_tech_phrases: - "certificate of public convenience and necessity" - "high voltage" From 4a63dd44aec6873baeed37f9073a9b23c42c7ff4 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 11:50:49 -0600 Subject: [PATCH 080/103] Modified extraction_system_prompt to include franchise It gets more complex but the overall collection is more efficient if we combine the regular ordinances with franchise in a single run. --- .../transmission/plugin_config.yaml | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 868b38310..4062d7483 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -100,4 +100,35 @@ heuristic_keywords: collection_prompts: true -cache_llm_generated_content: true +extraction_system_prompt: |- + You are a legal scholar extracting structured data from two + related types of ordinance documents: + (1) high-voltage electric transmission line ordinances, and + (2) electric utility franchise ordinances that grant a named + company the right to operate electric infrastructure in this + jurisdiction. + + For document type (1), be focused and literal: extract only + enacted, explicit, in-scope requirements that apply to + high-voltage (typically 69 kV or higher) electric transmission + lines, towers, poles, substations, and their rights-of-way. + Do not extract requirements that apply only to local electric + distribution, telecommunications, gas or oil pipelines, or + low-voltage service lines. + + For document type (2), extract only the "franchise" feature. + The summary must name every explicitly named grantee company + using its verbatim legal name and quote the granting clause. + Do not extract non-electric franchises (water, gas, cable, + telecom). Do not extract other schema features from a franchise + document unless the same document also contains explicit + transmission-line requirements. + + Be thorough and complete: review all relevant sections, + including tables and lists, so no explicitly stated in-scope + feature is missed. Before finalizing, perform a feature-coverage + check against the schema and ensure each explicitly supported + feature is captured exactly once. + Pay extra attention to any descriptions, instructions, examples, + and definitions in the schema, and follow all schema + instructions carefully. From 262c9c09db75aac20c1809533dbcfbc12fa231b8 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 12:03:37 -0600 Subject: [PATCH 081/103] Using subarea instead of scope for geographical sub-division Left this one behind when I transitioned from scope to subarea. --- compass/extraction/transmission/transmission_schema.json5 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index bde8c6d7e..9ae615635 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -8,7 +8,7 @@ "properties": { "outputs": { "type": "array", - "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per (feature, scope) pair**. Emit one row per scope only when the ordinance text explicitly states different values or requirements per scope for the same feature; otherwise emit a single row with scope='all'. Never infer, imply, or guess a requirement from related context.", + "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per (feature, subarea) pair**. Emit one row per subarea only when the ordinance text explicitly states different values or requirements per subarea for the same feature; otherwise emit a single row with subarea='all'. Never infer, imply, or guess a requirement from related context.", "items": { "type": "object", "required": [ @@ -233,10 +233,10 @@ } }, "prohibition_features": { - "description": "Prohibitions, bans, moratoria, or explicit siting restrictions on building, installing, or otherwise deploying electric transmission lines and related infrastructure within a specific zoning district. Use the `scope` field to identify the district. Extract only when the ordinance explicitly states that the restriction applies to transmission lines or related infrastructure. These features require text extraction only.", + "description": "Prohibitions, bans, moratoria, or explicit siting restrictions on building, installing, or otherwise deploying electric transmission lines and related infrastructure within a specific zoning district. Use the `subarea` field to identify the district. Extract only when the ordinance explicitly states that the restriction applies to transmission lines or related infrastructure. These features require text extraction only.", "properties": { "zone use restriction": { - "description": "Extract prohibitions, bans, moratoria, or explicit siting restrictions on electric transmission lines and related infrastructure within a specific zoning district. Set `scope` to the district being restricted (for example 'residential', 'commercial', 'agricultural'). Use scope='other' when the ordinance names a district that does not match the enumerated categories. Do not emit scope='all' for this feature; if the restriction is jurisdiction-wide, it is not a zone use restriction and should be captured under a different feature. Emit one row per explicitly restricted district. Include the verbatim ordinance language in summary and cite the restricting clause." + "description": "Extract prohibitions, bans, moratoria, or explicit siting restrictions on electric transmission lines and related infrastructure within a specific zoning district. Set `subarea` to the district being restricted (for example 'residential', 'commercial', 'agricultural'). Use subarea='other' when the ordinance names a district that does not match the enumerated categories. Do not emit subarea='all' for this feature; if the restriction is jurisdiction-wide, it is not a zone use restriction and should be captured under a different feature. Emit one row per explicitly restricted district. Include the verbatim ordinance language in summary and cite the restricting clause." } } }, From b21c112446b1f180f7a333fa7f457b7dbcc6dccf Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 23 Jul 2026 13:42:37 -0600 Subject: [PATCH 082/103] refact: Convert _apply_subarea_defaults to staticmethod (ruff no-self-use) --- compass/plugin/one_shot/components.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compass/plugin/one_shot/components.py b/compass/plugin/one_shot/components.py index 963926426..a0e239c45 100644 --- a/compass/plugin/one_shot/components.py +++ b/compass/plugin/one_shot/components.py @@ -558,7 +558,8 @@ def _to_dataframe(self, data): ] return full_df[["feature", *out_cols, "quantitative"]] - def _apply_subarea_defaults(self, df): + @staticmethod + def _apply_subarea_defaults(df): """Fill placeholder subarea entries with the 'all' sentinel""" if "subarea" not in df.columns: From f64a275318c2abcd078f19c1662a6d788b9e9f14 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 26 Jul 2026 19:44:13 -0600 Subject: [PATCH 083/103] fix: Another scope instead of subarea left behind --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 9ae615635..a323f60fe 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -335,7 +335,7 @@ "Set subarea='other' when the ordinance names a specific district that does not match any enumerated category (for example PUD, aviation overlay, conservation-2).", "Do not invent subareas from section headings, chapter titles, or nearby context that does not explicitly bind the requirement to that district.", "When the same feature has different requirements in different districts, emit one row per district; do not merge them into a single row.", - "For the 'zone use restriction' feature, scope must be a specific district (never 'all'); emit one row per explicitly restricted district.", + "For the 'zone use restriction' feature, subarea must be a specific district (never 'all'); emit one row per explicitly restricted district.", ], }, "$qualitative_features": [ From ce21a16e1fba31f96a95585fa27d27d8e0781ce7 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 27 Jul 2026 09:31:46 -0600 Subject: [PATCH 084/103] Copying query idea from GHP tech --- compass/extraction/transmission/plugin_config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 4062d7483..c86079585 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -12,6 +12,7 @@ query_templates: - "{jurisdiction}, high voltage transmission line ordinance" - "{jurisdiction}, overhead electric utility line zoning" - "{jurisdiction}, power line" + - "Where can I find the legal text for electric transmission line zoning ordinances in {jurisdiction}?" - "{jurisdiction}, electric utility franchise ordinance" - "{jurisdiction}, code of ordinances electric franchise" - "filetype:pdf {jurisdiction}, electric franchise agreement" From 849d3392ce5b7b0a793228698d5d25dc14378fd9 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 27 Jul 2026 09:35:20 -0600 Subject: [PATCH 085/103] Adding keywords for franchise --- compass/extraction/transmission/plugin_config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index c86079585..d989e8e43 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -72,6 +72,8 @@ heuristic_keywords: - "extra high voltage" - "extra-high voltage" - "bulk transmission" + - "electric franchise" + - "electric utility franchise" - "electric transmission" - "69 kv" - "115 kv" From 9ba9b9881afedabbabc7f59d057a707f45596ac5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 27 Jul 2026 09:46:50 -0600 Subject: [PATCH 086/103] doc: Explicit "high-voltage" --- compass/extraction/transmission/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/__init__.py b/compass/extraction/transmission/__init__.py index e0c458fc0..c2579dc37 100644 --- a/compass/extraction/transmission/__init__.py +++ b/compass/extraction/transmission/__init__.py @@ -1,4 +1,4 @@ -"""COMPASS transmission extraction plugin""" +"""COMPASS high-voltage transmission extraction plugin""" import importlib.resources From 2f7f9b93aab826d3d4eb4f516e61431d386a7a75 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 27 Jul 2026 15:06:00 -0600 Subject: [PATCH 087/103] clean: Unused old stuff left behind --- compass/utilities/finalize.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index 00f14a52c..e96f27c2e 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -16,31 +16,6 @@ logger = logging.getLogger(__name__) -_PARSED_COLS = [ - # TODO: Put these in an enum - "county", - "state", - "subdivision", - "jurisdiction_type", - "FIPS", - "feature", - "subarea", - "value", - "units", - "adder", - "min_dist", - "max_dist", - "summary", - "year", - "section", - "source", - "quantitative", -] -QUANT_OUT_COLS = _PARSED_COLS[:-1] -"""Output columns in quantitative ordinance file""" -QUAL_OUT_COLS = _PARSED_COLS[:7] + _PARSED_COLS[-5:-1] -"""Output columns in qualitative ordinance file""" - def save_run_meta( dirs, From 9ac6e26dbf1a356b59954a5c70cf9e4ee708e6f8 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Mon, 27 Jul 2026 19:25:19 -0600 Subject: [PATCH 088/103] style: --- compass/utilities/finalize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index e96f27c2e..975dae175 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) + def save_run_meta( dirs, tech, From 3c37d8c51e20e45c3d293cc38418041808017d97 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 29 Jul 2026 20:24:48 -0600 Subject: [PATCH 089/103] feat: Adding scope_context to core_principles --- compass/extraction/transmission/transmission_schema.json5 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index a323f60fe..abfd66f26 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -150,10 +150,9 @@ } }, "$core_principles": { - /* "scope_context": { + "description": "Only extract features that apply to high-voltage electric transmission lines and related infrastructure. Provisions scoped only to other energy systems (geothermal, solar PV, wind turbines) or only to lower-voltage electric infrastructure (distribution lines, service lines, service drops, secondary lines, sub-transmission) are out of scope unless the ordinance explicitly extends them to transmission. Encompassment rule: provisions that regulate a broader class which includes high-voltage transmission — for example 'all development', 'all construction activity', 'all public utilities', 'all utility infrastructure', 'all subdivision improvements', or 'electric utility lines' without a voltage qualifier — are in scope. If the ordinance defines 'transmission line' by a minimum voltage, treat that threshold as the cutoff: provisions explicitly restricted to voltages at or below it are out of scope even when phrased as electric utility infrastructure." }, - */ "strict_evidence_gate": { "description": "Extract a feature only when the ordinance text explicitly states a requirement, definition, or prohibition for that same feature. Never infer, assume, extrapolate, or guess from related context, implications, headings, or nearby provisions. If the ordinance points to an outside document or standard without restating the controlling requirement in the ordinance text itself, do not import missing values from that outside source." }, From 5189bd4e40ecafbd3f649b8ca1520b52b6af64bf Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 29 Jul 2026 20:46:40 -0600 Subject: [PATCH 090/103] schema: clarify structures vs residential building setback classification - Tighten 'structures distance' description to explicitly include generic 'building setback line' clauses tied to a transmission-line easement, right-of-way, or centerline when the building is not qualified as residential. - Tighten 'residential buildings distance' description with an exclusion redirecting generic unqualified building setback lines to 'structures distance'. - Add a $examples entry demonstrating a voltage-tiered building setback table (Roane County TN 5.2.6.2 pattern), routing it to 'structures distance' with subarea='all' and applying numeric prioritization to select the largest tier (75 ft). - Uncomment definition_v_rule_test in $core_principles. --- .../transmission/transmission_schema.json5 | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index abfd66f26..f4f0d710f 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -162,20 +162,19 @@ "numeric_prioritization": { "description": "When multiple numeric values apply to the same (feature, subarea) pair, keep one row for that pair and select the controlling most restrictive value. Restrictiveness rules: setbacks -> choose the largest minimum separation distance; maximum tower height and maximum structure height -> choose the lowest limit within each feature independently, never combine tower limits with accessory-structure limits; noise -> choose the lowest allowed noise limit. When the ordinance explicitly states different numeric limits per zoning district for the same feature (for example a noise table with distinct limits for residential vs commercial districts), emit one row per district using the appropriate `subarea` value and apply restrictiveness within each row. Keep condition-specific alternatives in summary only when the ordinance text explicitly shows they all apply to the same (feature, subarea) pair for electric transmission systems." }, - /* "definition_v_rule_test": { + "description": "Apply a primary-purpose routing test before classifying a clause as a definition. If the excerpt's main function is to state what a term means and it does not impose an enforceable obligation, extract it as a definition feature with value=null and units=null. If the excerpt imposes standards, applicability limits, permitting conditions, performance requirements, or prohibitions, classify it under the relevant operational or prohibition feature instead, even if the clause also names or restates a term." } - */ }, "$definitions": { "setback_features": { "description": "Setback features for electrical transmission lines and related infrastructure. Treat each setback feature independently and do not cross-apply a setback unless the ordinance text explicitly states that it applies to multiple target types. When a single clause explicitly lists multiple target types and one shared numeric setback, emit one row per explicitly listed feature using the same numeric value and units and cite the same clause in summary. Apply the shared numeric prioritization rules in $core_principles when multiple numeric values explicitly apply to the same feature.", "properties": { "structures distance": { - "description": "Minimum required separation from structures and buildings that do not match the definition of 'residential buildings distance'." + "description": "Minimum required separation from structures and buildings that do not match the definition of 'residential buildings distance'. Includes generic 'building setback line' clauses tied to a transmission-line easement, right-of-way, or centerline when the ordinance does not explicitly qualify the building as residential (for example 'residential', 'dwelling', 'home', 'residence')." }, "residential buildings distance": { - "description": "Minimum required separation from residential buildings, occupied dwellings, occupied buildings, residences, homes, residential receptors, or residential uses." + "description": "Minimum required separation from residential buildings, occupied dwellings, occupied buildings, residences, homes, residential receptors, or residential uses. Do not classify a generic unqualified 'building setback line' from a transmission-line easement as residential; that belongs under 'structures distance'." }, "property lines distance": { "description": "Minimum required separation from property lines, lot lines, parcel boundaries, or lease boundaries when the ordinance explicitly states the distance is measured from that boundary. Do not remap property-line distances to roads or residential zones unless the text explicitly makes them equivalent for that requirement. Distances to official plan lines or specific plan lines for public highways do not belong here unless the ordinance expressly defines those lines as property boundaries for the same requirement." @@ -316,6 +315,15 @@ "section": "Sec. 12.04.A – Prohibited Uses in Residential Districts", "summary": "Overhead electric transmission lines rated above 69 kV are prohibited in all residential districts (R-1, R-2, R-3).", "explanation": "Ordinance explicitly bans transmission lines from residential districts; subarea='residential' captures the district class." + }, + { + "feature": "structures distance", + "subarea": "all", + "value": 75, + "units": "feet", + "section": "5.2.6.2 Building Setback Lines", + "summary": "In the case of electric transmission lines where easement widths are not definitely established, there shall be a minimum building setback line from the center of the transmission line as follows: Voltage of Line / Minimum Building Setback — 46 KV: 37½ ft; 69 KV: 50 ft; 161 KV & over: 75 ft.", + "explanation": "Generic 'building setback line' measured from the transmission-line centerline, not qualified as residential, so classified as 'structures distance' rather than 'residential buildings distance'. Ordinance lists three voltage tiers (37½, 50, 75 feet); per numeric_prioritization for setbacks, the largest tier (75 feet) is selected as the row value while the full table is quoted verbatim in summary." } ], "$instructions": { From 86a8df62b39911c0218827c560e950d2c0759ed5 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 29 Jul 2026 20:54:49 -0600 Subject: [PATCH 091/103] schema: broaden bond feature to subdivision-scale utility bonds - Extend 'bond' description to include subdivision-scale utility-infrastructure completion bonds when the provision does not carve out electric infrastructure and is not restricted to lower-voltage electric infrastructure only (distribution, service, secondary, sub-transmission). - Clarify that qualitative bond rows are permitted; a numeric amount is not required when the provision imposes an enforceable obligation. --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index f4f0d710f..6dacf726a 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -217,7 +217,7 @@ "description": "Operational, licensing, permitting, inspection, and abandonment requirements. These features require text extraction only.", "properties": { "bond": { - "description": "Extract requirements for bonds, sureties, or financial assurance instruments related to construction, operation, maintenance, or decommissioning of electric transmission lines and related infrastructure. Include any specific conditions, amounts, or criteria for bond release when explicitly stated." + "description": "Extract requirements for bonds, sureties, or financial assurance instruments related to construction, operation, maintenance, or decommissioning of electric transmission lines and related infrastructure. Include any specific conditions, amounts, or criteria for bond release when explicitly stated. Also includes bond, surety, or financial-assurance provisions covering subdivision-scale utility-infrastructure completion when the provision does not carve out electric infrastructure and is not restricted to lower-voltage electric infrastructure only (distribution, service, secondary, sub-transmission lines). A numeric amount is not required; qualitative bond rows (for example 'sufficiency determined by the utility agency') are permitted when the provision imposes an enforceable obligation." }, "decommissioning": { "description": "Extract requirements for abandonment, removal, and site restoration and when systems are retired, fail, or towers are abandoned, including responsible party and timeline details when explicitly stated." From 408b955f9bb2c43e59035cf816a8e30113a04d64 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Wed, 29 Jul 2026 23:33:12 -0600 Subject: [PATCH 092/103] schema: add $scope key for per-chunk admission gate Introduce first-class $scope key consumed by COMPASS's dedicated per-chunk scope-check step (see compass/plugin/one_shot/components.py). This gate renders only the $scope string (not the full schema) into its prompt and admits chunks with inclusive-by-default framing, so mixed-voltage setback tables that include HV tiers pass through to the content-relevance filter instead of being rejected wholesale. --- compass/extraction/transmission/transmission_schema.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 6dacf726a..7d11c66c1 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -149,6 +149,7 @@ } } }, + "$scope": "Definitions for 'electric transmission line', 'transmission right-of-way', 'transmission easement', 'transmission tower', 'transmission substation', 'high-voltage line', or similar terms **OR** any legally enacted requirements, prohibitions, or permitting conditions that apply to high-voltage electric transmission lines, towers, poles, substations, or their rights-of-way/easements, **OR** any electric-utility franchise ordinance granting a named company the right to operate electric infrastructure in this jurisdiction. Provisions that by silence or by general wording (for example 'all public utilities', 'all utility lines and easements', 'all subdivision improvements', or a building-setback table tiered by voltage that includes any tier at or above 69 kV) would apply to electric transmission are IN SCOPE. **OUT OF SCOPE**: provisions explicitly and exclusively limited to electric distribution, service drops, secondary lines, or sub-transmission lines with no HV applicability; provisions scoped solely to non-electric utilities (water, gas, sewer, storm, telecom, cable); rules for other energy technologies (geothermal, solar PV, wind, hydro) unless the same clause also governs electric transmission. Proposed, future, draft, or contingent requirements that are not currently effective as written are also out of scope.", "$core_principles": { "scope_context": { "description": "Only extract features that apply to high-voltage electric transmission lines and related infrastructure. Provisions scoped only to other energy systems (geothermal, solar PV, wind turbines) or only to lower-voltage electric infrastructure (distribution lines, service lines, service drops, secondary lines, sub-transmission) are out of scope unless the ordinance explicitly extends them to transmission. Encompassment rule: provisions that regulate a broader class which includes high-voltage transmission — for example 'all development', 'all construction activity', 'all public utilities', 'all utility infrastructure', 'all subdivision improvements', or 'electric utility lines' without a voltage qualifier — are in scope. If the ordinance defines 'transmission line' by a minimum voltage, treat that threshold as the cutoff: provisions explicitly restricted to voltages at or below it are out of scope even when phrased as electric utility infrastructure." From a6adc1269ba7e8268d209fcde48179f7c9ae6584 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 30 Jul 2026 08:07:01 -0600 Subject: [PATCH 093/103] refact: Single & double quotes on queries Using double quotes to define jurisdiction context, then using single quotes as overall query. It used to cause errors using the single quotes. --- .../transmission/plugin_config.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index d989e8e43..32780a4fa 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -5,17 +5,17 @@ data_type_short_desc: high-voltage transmission lines ordinance doc_selection_method: "multi doc mixed" query_templates: - - "site:municode.com {jurisdiction}, electrical transmission line" - - "site:codelibrary.amlegal.com {jurisdiction}, electrical transmission line" - - "filetype:pdf {jurisdiction}, electric transmission line ordinance" - - "{jurisdiction}, zoning code electric transmission line setback" - - "{jurisdiction}, high voltage transmission line ordinance" - - "{jurisdiction}, overhead electric utility line zoning" - - "{jurisdiction}, power line" - - "Where can I find the legal text for electric transmission line zoning ordinances in {jurisdiction}?" - - "{jurisdiction}, electric utility franchise ordinance" - - "{jurisdiction}, code of ordinances electric franchise" - - "filetype:pdf {jurisdiction}, electric franchise agreement" + - 'site:municode.com "{jurisdiction}" electrical transmission line' + - 'site:codelibrary.amlegal.com "{jurisdiction}" electrical transmission line' + - '"{jurisdiction}" zoning code electric transmission line setback' + - '"{jurisdiction}" high voltage transmission line ordinance' + - '"{jurisdiction}" overhead electric utility line zoning' + - '"{jurisdiction}" power line' + - 'filetype:pdf "{jurisdiction}" electric transmission line ordinance' + - 'Where can I find the legal text for electric transmission line zoning ordinances in "{jurisdiction}"?' + - '"{jurisdiction}" electric utility franchise ordinance' + - '"{jurisdiction}" code of ordinances electric franchise' + - 'filetype:pdf "{jurisdiction}" electric franchise agreement' website_keywords: pdf: 92160 From 2df2ab584a0c5e09acc57d27ec96394ad26d3bfd Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Thu, 30 Jul 2026 21:31:45 -0600 Subject: [PATCH 094/103] fix: drop phrase-lock on site:-scoped queries The site:municode.com and site:codelibrary.amlegal.com queries were returning zero results for jurisdictions where the publisher indexes under an alternate name than `full_name` (e.g., 'Evangeline Parish Police Jury, LA' on municode vs. 'Evangeline Parish, Louisiana' in COMPASS). The phrase-lock is retained on free-form queries where it prevents cross-state noise (Humphreys TN was previously polluted by Moab UT before quoting was introduced). On site:-scoped queries the domain restriction already bounds the result set to a legitimate ordinance publisher, so phrase-locking adds no protection and blocks legitimate matches whenever the publisher's title differs from full_name. --- compass/extraction/transmission/plugin_config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 32780a4fa..139c949be 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -5,8 +5,8 @@ data_type_short_desc: high-voltage transmission lines ordinance doc_selection_method: "multi doc mixed" query_templates: - - 'site:municode.com "{jurisdiction}" electrical transmission line' - - 'site:codelibrary.amlegal.com "{jurisdiction}" electrical transmission line' + - 'site:municode.com {jurisdiction} electrical transmission line' + - 'site:codelibrary.amlegal.com {jurisdiction} electrical transmission line' - '"{jurisdiction}" zoning code electric transmission line setback' - '"{jurisdiction}" high voltage transmission line ordinance' - '"{jurisdiction}" overhead electric utility line zoning' From 9e0b52b131026a4f728949357b4045c9c8437f6a Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Fri, 31 Jul 2026 22:08:04 -0600 Subject: [PATCH 095/103] refact: Extending keywords --- .../transmission/plugin_config.yaml | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index 139c949be..e472f065b 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -58,6 +58,8 @@ heuristic_keywords: - "ehv" - "uhv" - "cpcn" # Certificate of Public Convenience and Necessity + - "hv" + - "kva" good_tech_keywords: - "transmission" - "kilovolt" @@ -65,12 +67,17 @@ heuristic_keywords: - "franchise" - "grantee" - "substation" + - "pole" + - "overhead" + - "right-of-way" + - "right of way" + - "rights-of-way" + - "utility line" good_tech_phrases: - "certificate of public convenience and necessity" - "high voltage" - - "high-voltage" - "extra high voltage" - - "extra-high voltage" + - "bulk electric" - "bulk transmission" - "electric franchise" - "electric utility franchise" @@ -83,6 +90,31 @@ heuristic_keywords: - "345 kv" - "500 kv" - "765 kv" + - "transmission line" + - "transmission pole" + - "electric line" + - "utility easement" + - "transmission or distribution" + - "electric transmission facilities" + - "overhead line" + - "overhead electric" + - "aerial line" + - "high-voltage line" + - "overhead electric line" + - "overhead transmission" + - "overhead power line" + - "transmission tower" + - "transmission facility" + - "utility transmission" + - "transmission corridor" + - "transmission right-of-way" + - "transmission right of way" + - "transmission easement" + - "transmission structure" + - "transmission infrastructure" + - "electric power line" + - "power line" + - "electric utility" not_tech_words: - "local distribution" - "transmission fluid" From 0d9aa087e9bcb675a62faa31c2f6dbc9618bfe81 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 2 Aug 2026 16:57:40 -0600 Subject: [PATCH 096/103] refact: Reframe transmission extraction prompt for franchise encompassment Treat transmission-line and franchise content as two kinds of content that may appear separately or together in one document, rather than as two distinct document types. Keep franchise as the primary output with its verbatim-name and granting-clause guidance, and allow content-(1) features to be extracted from a franchise document when provisions apply to the grantee's electric infrastructure and, by encompassment, would apply to its high-voltage transmission lines. --- .../transmission/plugin_config.yaml | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/compass/extraction/transmission/plugin_config.yaml b/compass/extraction/transmission/plugin_config.yaml index e472f065b..a1ccb8ade 100644 --- a/compass/extraction/transmission/plugin_config.yaml +++ b/compass/extraction/transmission/plugin_config.yaml @@ -136,28 +136,32 @@ heuristic_keywords: collection_prompts: true extraction_system_prompt: |- - You are a legal scholar extracting structured data from two - related types of ordinance documents: - (1) high-voltage electric transmission line ordinances, and - (2) electric utility franchise ordinances that grant a named - company the right to operate electric infrastructure in this + You are a legal scholar extracting structured data from + ordinance documents. A document may contain two kinds of + content, which can appear separately or together in the same + document: + (1) high-voltage electric transmission line requirements, and + (2) an electric utility franchise that grants a named company + the right to operate electric infrastructure in this jurisdiction. - For document type (1), be focused and literal: extract only - enacted, explicit, in-scope requirements that apply to - high-voltage (typically 69 kV or higher) electric transmission - lines, towers, poles, substations, and their rights-of-way. - Do not extract requirements that apply only to local electric - distribution, telecommunications, gas or oil pipelines, or - low-voltage service lines. + For transmission-line content (1), be focused and literal: + extract only enacted, explicit, in-scope requirements that + apply to high-voltage (typically 69 kV or higher) electric + transmission lines, towers, poles, substations, and their + rights-of-way. Do not extract requirements that apply only to + local electric distribution, telecommunications, gas or oil + pipelines, or low-voltage service lines. - For document type (2), extract only the "franchise" feature. - The summary must name every explicitly named grantee company + For franchise content (2), the "franchise" feature is the + primary output: name every explicitly named grantee company using its verbatim legal name and quote the granting clause. Do not extract non-electric franchises (water, gas, cable, - telecom). Do not extract other schema features from a franchise - document unless the same document also contains explicit - transmission-line requirements. + telecom). If the same document also contains transmission-line + content (1), extract those features as well under the rules + above, including provisions that apply to the grantee's + electric infrastructure generally and, by encompassment, would + apply to its high-voltage transmission lines. Be thorough and complete: review all relevant sections, including tables and lists, so no explicitly stated in-scope From d644e1862f8f75a28640fef2cf9168e20987d002 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 2 Aug 2026 17:17:22 -0600 Subject: [PATCH 097/103] refact: Broaden decommissioning definition for franchise removal Extend the decommissioning feature to cover obligations to remove facilities and restore the site upon termination, forfeiture, or expiration of a franchise or grant, in addition to abandonment on retirement or failure. The global encompassment_scope rules supply the voltage floor. --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 7d11c66c1..640a8e5da 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -221,7 +221,7 @@ "description": "Extract requirements for bonds, sureties, or financial assurance instruments related to construction, operation, maintenance, or decommissioning of electric transmission lines and related infrastructure. Include any specific conditions, amounts, or criteria for bond release when explicitly stated. Also includes bond, surety, or financial-assurance provisions covering subdivision-scale utility-infrastructure completion when the provision does not carve out electric infrastructure and is not restricted to lower-voltage electric infrastructure only (distribution, service, secondary, sub-transmission lines). A numeric amount is not required; qualitative bond rows (for example 'sufficiency determined by the utility agency') are permitted when the provision imposes an enforceable obligation." }, "decommissioning": { - "description": "Extract requirements for abandonment, removal, and site restoration and when systems are retired, fail, or towers are abandoned, including responsible party and timeline details when explicitly stated." + "description": "Extract requirements for abandonment, removal, and site restoration when systems are retired, fail, or towers are abandoned, including obligations to remove facilities and restore the site upon termination, forfeiture, or expiration of a franchise or grant, with responsible party and timeline details when explicitly stated." }, "permit": { "description": "Extract requirements for conditional use permits, special use permits, or other discretionary land use approvals that apply to electric transmission lines and related infrastructure. Include any specific conditions or criteria for approval when explicitly stated." From d8c5762126af945cd40f26cdb7d5064ef99565b6 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 2 Aug 2026 17:25:56 -0600 Subject: [PATCH 098/103] feat: Add traffic disruption definition Define the traffic disruption feature under operational_features. This closes the gap that caused the feature to be missed on franchise documents. --- compass/extraction/transmission/transmission_schema.json5 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 640a8e5da..348360403 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -228,6 +228,9 @@ }, "FAA": { "description": "Extract requirements for compliance with Federal Aviation Administration (FAA) regulations and local approvals for transmission lines and related infrastructure near airports. Include any specific FAA standards or local approval processes when explicitly stated." + }, + "traffic disruption": { + "description": "Extract requirements for how transmission infrastructure and related work affect public travel ways, temporarily or permanently: construction-period obligations not to obstruct traffic and to restore the right-of-way after excavation, road cuts, or street openings; and permanent effects of new construction such as line or crossing clearances over roadways, sight-distance at intersections and driveways, and limits on obstruction of the traveled way. Include timing and responsible-party details when explicitly stated." } } }, From a3c0ea6b148b61d8acdf1b06d05017bf0608f2a7 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 2 Aug 2026 17:28:20 -0600 Subject: [PATCH 099/103] refact: Broaden permit definition for right-of-way approvals Extend the permit feature beyond discretionary land-use approvals to also cover street-opening, road-cut, right-of-way, and encroachment permits issued by a road or public-works authority. The global encompassment_scope rules supply the voltage floor. --- compass/extraction/transmission/transmission_schema.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 348360403..4ebd986dc 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -224,7 +224,7 @@ "description": "Extract requirements for abandonment, removal, and site restoration when systems are retired, fail, or towers are abandoned, including obligations to remove facilities and restore the site upon termination, forfeiture, or expiration of a franchise or grant, with responsible party and timeline details when explicitly stated." }, "permit": { - "description": "Extract requirements for conditional use permits, special use permits, or other discretionary land use approvals that apply to electric transmission lines and related infrastructure. Include any specific conditions or criteria for approval when explicitly stated." + "description": "Extract requirements to obtain approval or authorization before constructing, installing, or working on electric transmission lines and related infrastructure, including conditional use permits, special use permits, or other discretionary land-use approvals, as well as street-opening, road-cut, right-of-way, or encroachment permits issued by a road or public-works authority. Include any specific conditions or criteria for approval when explicitly stated." }, "FAA": { "description": "Extract requirements for compliance with Federal Aviation Administration (FAA) regulations and local approvals for transmission lines and related infrastructure near airports. Include any specific FAA standards or local approval processes when explicitly stated." From a1a09de1491322e2dc37189a3b52f03d985da4f1 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Sun, 2 Aug 2026 22:55:18 -0600 Subject: [PATCH 100/103] refact: Collect and extract codified electric-franchise lists Broaden the transmission schema 'franchise' feature so a codified 'List of Franchises' index that names electric utilities as grantees (grantee, duration, originating-ordinance reference) is treated as substantive content to collect and extract, rather than being dropped by the content gate as a citation-only index. Add a matching $examples row modeled on the St. Helena Parish electric-franchise list so the collection gate has a positive exemplar. --- .../extraction/transmission/transmission_schema.json5 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 4ebd986dc..be05d34d5 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -278,7 +278,7 @@ "description": "Administrative or legal grants recorded in the ordinance code that do not impose a physical or numeric requirement on transmission infrastructure. Extract only when the ordinance text explicitly names the grant and its holder(s).", "properties": { "franchise": { - "description": "Electric utility franchise granted by this jurisdiction to a named company or companies. Extract only when the ordinance explicitly grants (or renews, extends, or amends) an electric franchise, right, or privilege to operate electric infrastructure in the jurisdiction. In summary, name every explicitly named grantee company using its verbatim legal name and quote the granting clause. Do not extract water, gas, cable, telecom, or other non-electric franchises. Do not infer a franchise from operational references to a utility elsewhere in the code. Emit at most one row per jurisdiction; if multiple electric franchises are named, list all grantees in the same summary." + "description": "Electric utility franchise granted by this jurisdiction to a named company or companies. Extract when the ordinance explicitly grants (or renews, extends, or amends) an electric franchise, right, or privilege to operate electric infrastructure, or when a codified list of franchises names one or more electric utilities as grantees under an electric-franchise heading, even if the full granting-clause text is omitted; treat such an electric-franchise listing as substantive content to collect and extract, not a citation-only index. In summary, name every electric grantee using its verbatim legal name and quote the granting clause or list entry. Do not extract water, gas, cable, telecom, or other non-electric franchises, even when listed alongside the electric ones. Do not infer a franchise from operational references to a utility elsewhere in the code. Emit at most one row per jurisdiction, listing all electric grantees in the same summary." } } } @@ -328,6 +328,15 @@ "section": "5.2.6.2 Building Setback Lines", "summary": "In the case of electric transmission lines where easement widths are not definitely established, there shall be a minimum building setback line from the center of the transmission line as follows: Voltage of Line / Minimum Building Setback — 46 KV: 37½ ft; 69 KV: 50 ft; 161 KV & over: 75 ft.", "explanation": "Generic 'building setback line' measured from the transmission-line centerline, not qualified as residential, so classified as 'structures distance' rather than 'residential buildings distance'. Ordinance lists three voltage tiers (37½, 50, 75 feet); per numeric_prioritization for setbacks, the largest tier (75 feet) is selected as the row value while the full table is quoted verbatim in summary." + }, + { + "feature": "franchise", + "subarea": "all", + "value": ["Dixie Electric Membership Corporation", "Louisiana Power and Light Company"], + "units": null, + "section": "Ch. 8 Franchises — List of Franchises", + "summary": "Under the 'Electric Franchises' heading of the codified list of franchises: Dixie Electric Membership Corporation (99 years), Ordinance of May 10, 1939; Louisiana Power and Light Company (no time limit), Ordinance of February 10, 1937. The code omits the full text of these franchise ordinances and preserves only this listing of grantees, durations, and originating ordinances.", + "explanation": "The codification records the electric franchises only as a list naming the grantees, their durations, and the originating ordinances; this electric-franchise listing is substantive franchise data even though the full granting clause was omitted. Only entries under the 'Electric Franchises' heading are extracted; cable, gas, and pipeline entries in the same list are excluded." } ], "$instructions": { From 8e1c5788f94d242ee538cbb89720c1459b036032 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Fri, 7 Aug 2026 19:18:54 -0600 Subject: [PATCH 101/103] refact: Renaming zone to district use restriction --- .../transmission/transmission_schema.json5 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index be05d34d5..ebd15982d 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -70,7 +70,7 @@ // prohibition applies to (rural, residential, commercial, // light industrial, industrial, agricultural, mixed use, // or 'other' for named districts not covered by the enum). - "zone use restriction", + "district use restriction", // -- Exclusions -- "historic site", "critical species habitat", @@ -224,7 +224,7 @@ "description": "Extract requirements for abandonment, removal, and site restoration when systems are retired, fail, or towers are abandoned, including obligations to remove facilities and restore the site upon termination, forfeiture, or expiration of a franchise or grant, with responsible party and timeline details when explicitly stated." }, "permit": { - "description": "Extract requirements to obtain approval or authorization before constructing, installing, or working on electric transmission lines and related infrastructure, including conditional use permits, special use permits, or other discretionary land-use approvals, as well as street-opening, road-cut, right-of-way, or encroachment permits issued by a road or public-works authority. Include any specific conditions or criteria for approval when explicitly stated." + "description": "Extract requirements to obtain approval or authorization before constructing, installing, or working on electric transmission lines and related infrastructure, including conditional use permits, special use permits, or other discretionary land-use approvals, as well as street-opening, road-cut, right-of-way, or encroachment permits issued by a road or public-works authority. Per $instructions.encompassment_scope, also extract a permit governing a broader class that would include transmission (for example 'all development', 'all utilities', or any work within public streets or rights-of-way), even when transmission is not named. Include any specific conditions or criteria for approval when explicitly stated." }, "FAA": { "description": "Extract requirements for compliance with Federal Aviation Administration (FAA) regulations and local approvals for transmission lines and related infrastructure near airports. Include any specific FAA standards or local approval processes when explicitly stated." @@ -237,8 +237,8 @@ "prohibition_features": { "description": "Prohibitions, bans, moratoria, or explicit siting restrictions on building, installing, or otherwise deploying electric transmission lines and related infrastructure within a specific zoning district. Use the `subarea` field to identify the district. Extract only when the ordinance explicitly states that the restriction applies to transmission lines or related infrastructure. These features require text extraction only.", "properties": { - "zone use restriction": { - "description": "Extract prohibitions, bans, moratoria, or explicit siting restrictions on electric transmission lines and related infrastructure within a specific zoning district. Set `subarea` to the district being restricted (for example 'residential', 'commercial', 'agricultural'). Use subarea='other' when the ordinance names a district that does not match the enumerated categories. Do not emit subarea='all' for this feature; if the restriction is jurisdiction-wide, it is not a zone use restriction and should be captured under a different feature. Emit one row per explicitly restricted district. Include the verbatim ordinance language in summary and cite the restricting clause." + "district use restriction": { + "description": "Extract prohibitions, bans, moratoria, or explicit siting restrictions on electric transmission lines and related infrastructure within a specific zoning district. Set `subarea` to the district being restricted (for example 'residential', 'commercial', 'agricultural'). Use subarea='other' when the ordinance names a district that does not match the enumerated categories. Do not emit subarea='all' for this feature; if the restriction is jurisdiction-wide, it is not a district use restriction and should be captured under a different feature. Emit one row per explicitly restricted district. Include the verbatim ordinance language in summary and cite the restricting clause." } } }, @@ -312,7 +312,7 @@ "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other districts are also defined." }, { - "feature": "zone use restriction", + "feature": "district use restriction", "subarea": "residential", "value": null, "units": null, @@ -355,7 +355,7 @@ "Set subarea='other' when the ordinance names a specific district that does not match any enumerated category (for example PUD, aviation overlay, conservation-2).", "Do not invent subareas from section headings, chapter titles, or nearby context that does not explicitly bind the requirement to that district.", "When the same feature has different requirements in different districts, emit one row per district; do not merge them into a single row.", - "For the 'zone use restriction' feature, subarea must be a specific district (never 'all'); emit one row per explicitly restricted district.", + "For the 'district use restriction' feature, subarea must be a specific district (never 'all'); emit one row per explicitly restricted district.", ], }, "$qualitative_features": [ From 875278c95e5fd51797d9d10e9a8448ef43d4a677 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Fri, 7 Aug 2026 19:57:31 -0600 Subject: [PATCH 102/103] feat: Add aggregated boolean field to transmission extraction schema --- .../transmission/transmission_schema.json5 | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index ebd15982d..9ab372102 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -18,7 +18,8 @@ "units", "section", "summary", - "explanation" + "explanation", + "aggregated" ], "additionalProperties": false, "properties": { @@ -144,6 +145,10 @@ "explanation": { "type": "string", "description": "Brief rationale explaining why this row matches the selected feature under this schema. Reference the specific evidence in summary and how it supports the extracted value and units or, for qualitative features, the inclusion criteria. Must be a non-null, non-empty string and must not use absence placeholders." + }, + "aggregated": { + "type": "boolean", + "description": "True when this row's value was selected from multiple competing explicit values that apply to the same (feature, subarea) pair, keeping the most restrictive one per $core_principles.numeric_prioritization (most restrictive is the largest value for setbacks, the smallest value for noise and height limits, and so on depending on the feature). False when the value comes from a single explicit value with no competing values to reconcile." } } } @@ -291,7 +296,8 @@ "units": "dBA", "section": "SECTION 1308 – Performance / Construction Standards", "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance.", - "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single row with subarea='all' is emitted." + "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single row with subarea='all' is emitted.", + "aggregated": false }, { "feature": "noise", @@ -300,7 +306,8 @@ "units": "dBA", "section": "Table 5-3 – Noise Standards by District", "summary": "Maximum permissible operational noise at the property line for transmission facilities in residential districts is 45 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for residential districts; a companion row with subarea='commercial' captures the 55 dBA limit for commercial districts." + "explanation": "Ordinance explicitly states a distinct noise limit for residential districts; a companion row with subarea='commercial' captures the 55 dBA limit for commercial districts.", + "aggregated": false }, { "feature": "noise", @@ -309,7 +316,8 @@ "units": "dBA", "section": "Table 5-3 – Noise Standards by District", "summary": "Maximum permissible operational noise at the property line for transmission facilities in commercial districts is 55 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other districts are also defined." + "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other districts are also defined.", + "aggregated": false }, { "feature": "district use restriction", @@ -318,7 +326,8 @@ "units": null, "section": "Sec. 12.04.A – Prohibited Uses in Residential Districts", "summary": "Overhead electric transmission lines rated above 69 kV are prohibited in all residential districts (R-1, R-2, R-3).", - "explanation": "Ordinance explicitly bans transmission lines from residential districts; subarea='residential' captures the district class." + "explanation": "Ordinance explicitly bans transmission lines from residential districts; subarea='residential' captures the district class.", + "aggregated": false }, { "feature": "structures distance", @@ -327,7 +336,8 @@ "units": "feet", "section": "5.2.6.2 Building Setback Lines", "summary": "In the case of electric transmission lines where easement widths are not definitely established, there shall be a minimum building setback line from the center of the transmission line as follows: Voltage of Line / Minimum Building Setback — 46 KV: 37½ ft; 69 KV: 50 ft; 161 KV & over: 75 ft.", - "explanation": "Generic 'building setback line' measured from the transmission-line centerline, not qualified as residential, so classified as 'structures distance' rather than 'residential buildings distance'. Ordinance lists three voltage tiers (37½, 50, 75 feet); per numeric_prioritization for setbacks, the largest tier (75 feet) is selected as the row value while the full table is quoted verbatim in summary." + "explanation": "Generic 'building setback line' measured from the transmission-line centerline, not qualified as residential, so classified as 'structures distance' rather than 'residential buildings distance'. Ordinance lists three voltage tiers (37½, 50, 75 feet); per numeric_prioritization for setbacks, the largest tier (75 feet) is selected as the row value while the full table is quoted verbatim in summary.", + "aggregated": true }, { "feature": "franchise", @@ -336,7 +346,8 @@ "units": null, "section": "Ch. 8 Franchises — List of Franchises", "summary": "Under the 'Electric Franchises' heading of the codified list of franchises: Dixie Electric Membership Corporation (99 years), Ordinance of May 10, 1939; Louisiana Power and Light Company (no time limit), Ordinance of February 10, 1937. The code omits the full text of these franchise ordinances and preserves only this listing of grantees, durations, and originating ordinances.", - "explanation": "The codification records the electric franchises only as a list naming the grantees, their durations, and the originating ordinances; this electric-franchise listing is substantive franchise data even though the full granting clause was omitted. Only entries under the 'Electric Franchises' heading are extracted; cable, gas, and pipeline entries in the same list are excluded." + "explanation": "The codification records the electric franchises only as a list naming the grantees, their durations, and the originating ordinances; this electric-franchise listing is substantive franchise data even though the full granting clause was omitted. Only entries under the 'Electric Franchises' heading are extracted; cable, gas, and pipeline entries in the same list are excluded.", + "aggregated": false } ], "$instructions": { From cd1c337bd1746456ba7df6454044791e19ad9368 Mon Sep 17 00:00:00 2001 From: Gui Castelao Date: Fri, 7 Aug 2026 20:17:26 -0600 Subject: [PATCH 103/103] refactor: Remove subarea plumbing from one-shot plugin Drop the subarea concept in favor of flagging aggregated restuls t o run a two phase analysis. --- .../transmission/transmission_schema.json5 | 50 ++--------------- compass/plugin/one_shot/base.py | 23 -------- compass/plugin/one_shot/components.py | 13 ----- .../unit/plugin/test_plugin_one_shot.py | 55 ------------------- 4 files changed, 4 insertions(+), 137 deletions(-) diff --git a/compass/extraction/transmission/transmission_schema.json5 b/compass/extraction/transmission/transmission_schema.json5 index 9ab372102..e96580f3a 100644 --- a/compass/extraction/transmission/transmission_schema.json5 +++ b/compass/extraction/transmission/transmission_schema.json5 @@ -8,12 +8,11 @@ "properties": { "outputs": { "type": "array", - "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per (feature, subarea) pair**. Emit one row per subarea only when the ordinance text explicitly states different values or requirements per subarea for the same feature; otherwise emit a single row with subarea='all'. Never infer, imply, or guess a requirement from related context.", + "description": "Sparse long-form extraction table. Include only features with an enacted, explicit requirement and emit **at most one row per feature**. Never infer, imply, or guess a requirement from related context.", "items": { "type": "object", "required": [ "feature", - "subarea", "value", "units", "section", @@ -67,10 +66,6 @@ "FAA", // -- Prohibited or limited-use districts -- - // Use the `subarea` field to identify which district the - // prohibition applies to (rural, residential, commercial, - // light industrial, industrial, agricultural, mixed use, - // or 'other' for named districts not covered by the enum). "district use restriction", // -- Exclusions -- "historic site", @@ -103,19 +98,6 @@ "franchise", ] }, - "subarea": { - "type": "string", - "description": "The subset of the jurisdiction to which this row applies. Use 'all' when the requirement applies jurisdiction-wide with no zone-specific variation. Use one of the named zoning categories when the ordinance text explicitly ties the requirement to that district. Use 'other' when the ordinance names a specific district that does not match any of the enumerated categories (for example, 'PUD', 'aviation overlay', 'conservation-2'). Do not invent categories; do not infer a zone from headings or nearby context that does not explicitly bind the requirement to that district.", - "enum": [ - "rural", - "residential", - "commercial", - "light industrial", - "industrial", - "agricultural", - "mixed use" - ] - }, "value": { "description": "The extracted ordinance value. For numerical distance thresholds and limits, use a number. For categorical outcomes, use a string. For fields that list multiple named items, use an array of strings. Use null only for qualitative features, and only when an enacted, explicit, enforceable ordinance requirement for that feature is present. Null must never be used to indicate absence. If a feature has no enacted, explicit requirement in the ordinance text, omit that feature from outputs.", "anyOf": [ @@ -148,7 +130,7 @@ }, "aggregated": { "type": "boolean", - "description": "True when this row's value was selected from multiple competing explicit values that apply to the same (feature, subarea) pair, keeping the most restrictive one per $core_principles.numeric_prioritization (most restrictive is the largest value for setbacks, the smallest value for noise and height limits, and so on depending on the feature). False when the value comes from a single explicit value with no competing values to reconcile." + "description": "True when this row's value was selected from multiple competing explicit values for the same feature, keeping the most restrictive one per $core_principles.numeric_prioritization (most restrictive is the largest value for setbacks, the smallest value for noise and height limits, and so on depending on the feature). False when the value comes from a single explicit value with no competing values to reconcile." } } } @@ -291,47 +273,24 @@ "$examples": [ { "feature": "noise", - "subarea": "all", "value": 50, "units": "dBA", "section": "SECTION 1308 – Performance / Construction Standards", "summary": "The transmission line and or facility shall not generate noise in excess of Fifty (50) decibel levels at ground level to the property lines or at the nearest residence. Such measurements shall be signed by a qualified engineer, stating that noise levels are being met, per ordinance.", - "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide with no zone-specific variation, so a single row with subarea='all' is emitted.", - "aggregated": false - }, - { - "feature": "noise", - "subarea": "residential", - "value": 45, - "units": "dBA", - "section": "Table 5-3 – Noise Standards by District", - "summary": "Maximum permissible operational noise at the property line for transmission facilities in residential districts is 45 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for residential districts; a companion row with subarea='commercial' captures the 55 dBA limit for commercial districts.", - "aggregated": false - }, - { - "feature": "noise", - "subarea": "commercial", - "value": 55, - "units": "dBA", - "section": "Table 5-3 – Noise Standards by District", - "summary": "Maximum permissible operational noise at the property line for transmission facilities in commercial districts is 55 dBA.", - "explanation": "Ordinance explicitly states a distinct noise limit for commercial districts. Other districts are also defined.", + "explanation": "Ordinance sets a single numeric noise limit that applies to the transmission line jurisdiction-wide, so a single noise row is emitted.", "aggregated": false }, { "feature": "district use restriction", - "subarea": "residential", "value": null, "units": null, "section": "Sec. 12.04.A – Prohibited Uses in Residential Districts", "summary": "Overhead electric transmission lines rated above 69 kV are prohibited in all residential districts (R-1, R-2, R-3).", - "explanation": "Ordinance explicitly bans transmission lines from residential districts; subarea='residential' captures the district class.", + "explanation": "Ordinance explicitly bans transmission lines from residential districts.", "aggregated": false }, { "feature": "structures distance", - "subarea": "all", "value": 75, "units": "feet", "section": "5.2.6.2 Building Setback Lines", @@ -341,7 +300,6 @@ }, { "feature": "franchise", - "subarea": "all", "value": ["Dixie Electric Membership Corporation", "Louisiana Power and Light Company"], "units": null, "section": "Ch. 8 Franchises — List of Franchises", diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index c5f61e51f..59b683372 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -28,8 +28,6 @@ SchemaBasedTextCollector, SchemaBasedTextExtractor, SchemaOrdinanceParser, - SUBAREA_SENTINEL_ALL, - SUBAREA_SENTINEL_OTHER, ) from compass.plugin.one_shot.cache import key_from_cache, key_to_cache from compass.services.threaded import CLEANED_FP_REGISTRY @@ -54,25 +52,6 @@ class _CacheKey(StrEnum): HEURISTIC_KEYWORDS = auto() -def _inject_subarea_sentinels(schema): - """Add ``all``/``other`` sentinels to the subarea enum in-place""" - try: - subarea = schema["properties"]["outputs"]["items"]["properties"][ - "subarea" - ] - except (KeyError, TypeError): - return - - enum = subarea.get("enum") - if not isinstance(enum, list): - return - - if SUBAREA_SENTINEL_ALL not in enum: - enum.insert(0, SUBAREA_SENTINEL_ALL) - if SUBAREA_SENTINEL_OTHER not in enum: - enum.append(SUBAREA_SENTINEL_OTHER) - - def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore[complex-structure] """Create a one-shot extraction plugin based on a configuration @@ -214,8 +193,6 @@ def create_schema_based_one_shot_extraction_plugin(config, tech): # ruff:ignore if isinstance(config["schema"], str): config["schema"] = load_config(config["schema"]) - _inject_subarea_sentinels(config["schema"]) - config["qual_feats"] = { f.casefold() for f in config["schema"].pop("$qualitative_features", []) } diff --git a/compass/plugin/one_shot/components.py b/compass/plugin/one_shot/components.py index a0e239c45..ce016611d 100644 --- a/compass/plugin/one_shot/components.py +++ b/compass/plugin/one_shot/components.py @@ -17,9 +17,6 @@ logger = logging.getLogger(__name__) -SUBAREA_SENTINEL_ALL = "all" -SUBAREA_SENTINEL_OTHER = "other" - _DEFAULT_TEXT_SCOPE_SYSTEM_PROMPT = """\ You are a structured extraction scope validator. Given a text chunk, \ determine whether the chunk is within the given extraction scope. \ @@ -540,7 +537,6 @@ def _to_dataframe(self, data): {"feature": all_features, "quantitative": quant} ) full_df = full_df.merge(df, on="feature", how="left") - full_df = self._apply_subarea_defaults(full_df) ignore_cols = { "county", @@ -557,12 +553,3 @@ def _to_dataframe(self, data): if col.name in full_df.columns and col.name not in ignore_cols ] return full_df[["feature", *out_cols, "quantitative"]] - - @staticmethod - def _apply_subarea_defaults(df): - """Fill placeholder subarea entries with the 'all' sentinel""" - - if "subarea" not in df.columns: - df["subarea"] = SUBAREA_SENTINEL_ALL - df["subarea"] = df["subarea"].fillna(SUBAREA_SENTINEL_ALL) - return df diff --git a/tests/python/unit/plugin/test_plugin_one_shot.py b/tests/python/unit/plugin/test_plugin_one_shot.py index 00af501e5..bd5997d56 100644 --- a/tests/python/unit/plugin/test_plugin_one_shot.py +++ b/tests/python/unit/plugin/test_plugin_one_shot.py @@ -1,13 +1,11 @@ """COMPASS one-shot plugin tests""" -from copy import deepcopy from pathlib import Path import pytest from compass.exceptions import COMPASSPluginConfigurationError from compass.plugin.one_shot.base import ( - _inject_subarea_sentinels, _normalize_heuristic_keywords, ) from compass.plugin.one_shot.components import SchemaBasedTextCollector @@ -223,59 +221,6 @@ async def test_schema_text_collector_stores_chunk_after_scope_and_context(): 1: "operative geothermal setback text", } -def _schema_with_subarea(enum): - return { - "properties": { - "outputs": { - "items": { - "properties": {"subarea": {"enum": list(enum)}} - } - } - } - } - - -def test_inject_subarea_sentinels_wraps_enum(): - """``all`` is prepended and ``other`` appended around user values""" - - schema = _schema_with_subarea(["residential", "commercial"]) - _inject_subarea_sentinels(schema) - - enum = schema["properties"]["outputs"]["items"]["properties"][ - "subarea" - ]["enum"] - assert enum == ["all", "residential", "commercial", "other"] - - -def test_inject_subarea_sentinels_is_idempotent(): - """Calling twice does not duplicate sentinels""" - - schema = _schema_with_subarea(["residential"]) - _inject_subarea_sentinels(schema) - _inject_subarea_sentinels(schema) - - enum = schema["properties"]["outputs"]["items"]["properties"][ - "subarea" - ]["enum"] - assert enum == ["all", "residential", "other"] - - -def test_inject_subarea_sentinels_noop_when_no_subarea_property(): - """Legacy schemas without a subarea property are left unchanged""" - - schema = { - "properties": { - "outputs": { - "items": {"properties": {"feature": {"enum": ["a", "b"]}}} - } - } - } - original = deepcopy(schema) - - _inject_subarea_sentinels(schema) - - assert schema == original - if __name__ == "__main__": pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"])