@snaptrude/plugin-core 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,14 +10,17 @@ import { PluginAreaUnit } from "./metrics"
10
10
  * in Snaptrude units — the same plan space as a space's `planPoints`), and —
11
11
  * when the project is geo-located on terrain — their geographic
12
12
  * (latitude/longitude) rings. This is the program-planning view of the site;
13
- * zoning numbers (FAR/FSI, height limits) live in the site-analysis sheet and
14
- * are not read here with one exception: `listEdges` reads each parcel's
15
- * per-edge front/side/rear classification and effective base setbacks, the
16
- * same resolution the buildable-envelope setback pills show on canvas.
13
+ * zoning numbers (FAR/FSI, height limits) live in the Site Analysis sheet and
14
+ * are available through `getSiteAnalysis`, not the site-context reads. One
15
+ * context exception is `listEdges`: it reads each parcel's per-edge
16
+ * front/side/rear classification and effective base setbacks, the same
17
+ * resolution the buildable-envelope setback pills show on canvas.
17
18
  *
18
- * All methods are reads: they return plain records and never throw — `get`
19
- * returns an empty snapshot (zero totals) when there is no site, and the `list`
20
- * methods return `[]`.
19
+ * Site context methods are total reads: they return plain records and never
20
+ * throw — `get` returns an empty snapshot (zero totals) when there is no site,
21
+ * and the `list` methods return `[]`. Site Analysis is the exception:
22
+ * `getSiteAnalysis` reads the active proposal's persisted analysis sheet and
23
+ * `updateSiteAnalysis` performs a safe merge/append write to that sheet.
21
24
  *
22
25
  * Accessed via `snaptrude.program.site`.
23
26
  */
@@ -279,8 +282,126 @@ export abstract class PluginProgramSiteApi {
279
282
  * ```
280
283
  */
281
284
  public abstract getWeather(): PluginApiReturn<PluginProgramSiteWeatherResult>
285
+
286
+ /**
287
+ * Read the persisted Site Analysis sheet for the active proposal.
288
+ *
289
+ * The read works without Program mode open. When a Program tab is open, the
290
+ * host first makes a best-effort attempt to flush its pending edits. The
291
+ * active proposal's sheet is preferred, with the base `"Site Analysis"`
292
+ * sheet as fallback. Returns `null` when no parseable sheet is persisted.
293
+ *
294
+ * @returns The persisted site location, polygon, and constraint rows, or
295
+ * `null` when no Site Analysis data exists. This read never throws.
296
+ *
297
+ * @examplePrompt Read the current Site Analysis sheet
298
+ * @examplePrompt What zoning constraints are recorded for this proposal?
299
+ *
300
+ * # Example
301
+ * ```ts
302
+ * const analysis = await snaptrude.program.site.getSiteAnalysis()
303
+ * if (analysis) console.log(analysis.sheetName, analysis.rows)
304
+ * ```
305
+ */
306
+ public abstract getSiteAnalysis(): PluginApiReturn<PluginProgramSiteAnalysisResult>
307
+
308
+ /**
309
+ * Merge sourced site/zoning constraints into the active proposal's persisted
310
+ * Site Analysis sheet.
311
+ *
312
+ * Rows match by trimmed, case-insensitive Category + Description. Matched
313
+ * rows update only Quantity/Unit; unmatched rows append. Existing labels,
314
+ * formatting, and unrelated rows remain intact. There is deliberately no
315
+ * replace/reset mode — an appended duplicate row can NEVER be removed, so
316
+ * there is no "write now, fix later". Send only the rows that were actually
317
+ * sourced, ALWAYS call `getSiteAnalysis` first and reuse the sheet's exact
318
+ * labels, and never use `program.spreadsheet.setValues` for Site Analysis
319
+ * data — this method is the only correct writer. Check the result:
320
+ * `updatedRows` should account for every row that had a template match; an
321
+ * unexpected `appendedRows` usually means a label did not match the read's
322
+ * vocabulary.
323
+ *
324
+ * When Program mode is open, the host writes through the live sheet. Without
325
+ * a live tab it safely merges the persisted document. The call rejects rather
326
+ * than risk overwriting unflushed live edits or an unparseable existing sheet.
327
+ *
328
+ * @param rows - One or more sourced constraint rows to merge or append.
329
+ * @param options - Optional site location and JSON-encoded site polygon.
330
+ * @returns The target sheet name and merge counts.
331
+ * @throws PRECONDITION_FAILED when proposals exist but none is active, or
332
+ * when live Program edits cannot be flushed safely.
333
+ *
334
+ * @examplePrompt Save these zoning constraints to Site Analysis
335
+ * @examplePrompt Update the FAR and setbacks in the Site Analysis sheet
336
+ *
337
+ * # Example
338
+ * ```ts
339
+ * const result = await snaptrude.program.site.updateSiteAnalysis([
340
+ * { category: "FAR", description: "Floor Area Ratio", quantity: 3.5, unit: "ratio" },
341
+ * { category: "Set backs", description: "Front setbacks", quantity: 6, unit: "m" },
342
+ * ])
343
+ * console.log(result.sheetName, result.updatedRows, result.appendedRows)
344
+ * ```
345
+ */
346
+ public abstract updateSiteAnalysis(
347
+ rows: PluginSiteAnalysisRow[],
348
+ options?: PluginSiteAnalysisUpdateOptions,
349
+ ): PluginApiReturn<PluginProgramSiteAnalysisUpdateResult>
282
350
  }
283
351
 
352
+ /**
353
+ * A constraint row stored in the Site Analysis sheet. A `quantity` of `"-"`
354
+ * means the value is not recorded yet — never echo `"-"` back as a sourced
355
+ * value.
356
+ */
357
+ export const PluginSiteAnalysisRow = z.object({
358
+ category: z.string().min(1),
359
+ description: z.string(),
360
+ quantity: z.union([z.string(), z.number()]),
361
+ unit: z.string(),
362
+ })
363
+ export type PluginSiteAnalysisRow = z.infer<typeof PluginSiteAnalysisRow>
364
+
365
+ /**
366
+ * Optional site context persisted alongside Site Analysis constraint rows.
367
+ * `null` and `undefined` are both accepted as "no options".
368
+ */
369
+ export const PluginSiteAnalysisUpdateOptions = z
370
+ .object({
371
+ siteLocation: z.object({ lat: z.number(), lng: z.number() }).optional(),
372
+ /** JSON-encoded polygon string written to the Site Polygon row. */
373
+ sitePolygon: z.string().optional(),
374
+ })
375
+ .nullish()
376
+ export type PluginSiteAnalysisUpdateOptions = z.infer<
377
+ typeof PluginSiteAnalysisUpdateOptions
378
+ >
379
+
380
+ /** Persisted Site Analysis data for the active proposal. */
381
+ export const PluginProgramSiteAnalysisResult = z
382
+ .object({
383
+ sheetName: z.string(),
384
+ siteLocation: z.object({ lat: z.number(), lng: z.number() }).nullable(),
385
+ sitePolygon: z.string().nullable(),
386
+ rows: z.array(PluginSiteAnalysisRow),
387
+ })
388
+ .nullable()
389
+ export type PluginProgramSiteAnalysisResult = z.infer<
390
+ typeof PluginProgramSiteAnalysisResult
391
+ >
392
+
393
+ /** Result of a merge/append Site Analysis write. */
394
+ export const PluginProgramSiteAnalysisUpdateResult = z.object({
395
+ sheetName: z.string(),
396
+ /** Constraint rows on the sheet after the write. */
397
+ rowCount: z.number(),
398
+ updatedRows: z.number(),
399
+ appendedRows: z.number(),
400
+ })
401
+ export type PluginProgramSiteAnalysisUpdateResult = z.infer<
402
+ typeof PluginProgramSiteAnalysisUpdateResult
403
+ >
404
+
284
405
  /**
285
406
  * A 2D ground-plane point of a site parcel footprint, in world XZ plan
286
407
  * coordinates (Snaptrude units) — the same plan space as a space's
@@ -564,7 +685,9 @@ export type PluginProgramSiteWeatherResult = z.infer<
564
685
  * never appears — the backend synonym resolves to `front`.
565
686
  */
566
687
  export const PluginProgramSiteEdgeRole = z.enum(["front", "side", "rear"])
567
- export type PluginProgramSiteEdgeRole = z.infer<typeof PluginProgramSiteEdgeRole>
688
+ export type PluginProgramSiteEdgeRole = z.infer<
689
+ typeof PluginProgramSiteEdgeRole
690
+ >
568
691
 
569
692
  /**
570
693
  * One boundary edge of a site parcel, as {@linkcode PluginProgramSiteApi.listEdges}
package/src/handles.ts CHANGED
@@ -22,7 +22,7 @@ import type { ArenaKind } from "./api/core/handles"
22
22
  * handle eventually releases its host registry entry. Deterministic release is
23
23
  * still preferred: `core.handles.release/releaseAll` or scopes.
24
24
  *
25
- * Crossing non-RPC boundaries (popup UI postMessage, persistence, logging):
25
+ * Crossing non-RPC boundaries (UI postMessage, persistence, logging):
26
26
  * send {@linkcode Handle.id} — structured clone strips the class prototype.
27
27
  */
28
28
  export class Handle<K extends string> {
@@ -173,7 +173,7 @@ export type BBoxComponents = { min: Vec3Components; max: Vec3Components }
173
173
  * - the bare id string (the wire form — the client unwraps args to ids)
174
174
  * - a live `Handle` instance (host-internal re-parse, direct host callers)
175
175
  * - `{ __h: string }` (the tagged result form, echoed back as an arg)
176
- * - `{ id: string }` (a Handle stripped by structured clone at the popup-UI boundary)
176
+ * - `{ id: string }` (a Handle stripped by structured clone at the UI boundary)
177
177
  * Shape checking stays prefix-only; existence, kind, and ownership are enforced
178
178
  * host-side by the HandleRegistry (§9.7) — no existence oracle.
179
179
  */