@mindflight/mindbrain-personal-studio 0.6.2 → 0.6.3

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.
Files changed (202) hide show
  1. package/README.md +16 -8
  2. package/build/client/_app/immutable/chunks/{D0UIlUGZ.js → CIErFlYG.js} +1 -1
  3. package/build/client/_app/immutable/chunks/CIErFlYG.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/CIErFlYG.js.gz +0 -0
  5. package/build/client/_app/immutable/entry/{app.CR-imLox.js → app.mURYm8o_.js} +2 -2
  6. package/build/client/_app/immutable/entry/app.mURYm8o_.js.br +0 -0
  7. package/build/client/_app/immutable/entry/app.mURYm8o_.js.gz +0 -0
  8. package/build/client/_app/immutable/entry/start.D4M9ZeGO.js +1 -0
  9. package/build/client/_app/immutable/entry/start.D4M9ZeGO.js.br +2 -0
  10. package/build/client/_app/immutable/entry/start.D4M9ZeGO.js.gz +0 -0
  11. package/build/client/_app/immutable/nodes/{1.CTEedoSY.js → 1.DHKtMeFI.js} +1 -1
  12. package/build/client/_app/immutable/nodes/1.DHKtMeFI.js.br +1 -0
  13. package/build/client/_app/immutable/nodes/1.DHKtMeFI.js.gz +0 -0
  14. package/build/client/_app/version.json +1 -1
  15. package/build/client/_app/version.json.br +0 -0
  16. package/build/client/_app/version.json.gz +0 -0
  17. package/build/handler.js +4 -4
  18. package/build/index.js +4 -4
  19. package/build/server/chunks/chunks/{internal.js-WOmQXGMa.js → internal.js-C3tV0XXj.js} +2 -2
  20. package/build/server/chunks/chunks/{internal.js-WOmQXGMa.js.map → internal.js-C3tV0XXj.js.map} +1 -1
  21. package/build/server/chunks/{handler-BIDedSZq.js → handler-DlaCCnxx.js} +3 -3
  22. package/build/server/chunks/{handler-BIDedSZq.js.map → handler-DlaCCnxx.js.map} +1 -1
  23. package/build/server/chunks/{index.js-YVPJa0so.js → index.js-BGfKWHak.js} +2 -2
  24. package/build/server/chunks/{index.js-YVPJa0so.js.map → index.js-BGfKWHak.js.map} +1 -1
  25. package/build/server/chunks/{manifest.js-aGRKuiqF.js → manifest.js-CnmaNf5D.js} +3 -3
  26. package/build/server/chunks/{manifest.js-aGRKuiqF.js.map → manifest.js-CnmaNf5D.js.map} +1 -1
  27. package/build/server/chunks/nodes/{1.js-Dhh3ErZY.js → 1.js-BypjwBYB.js} +2 -2
  28. package/build/server/chunks/nodes/{1.js-Dhh3ErZY.js.map → 1.js-BypjwBYB.js.map} +1 -1
  29. package/examples/immeuble/ACCEPTANCE.yaml +82 -0
  30. package/examples/immeuble/BIM_LITE.md +15 -0
  31. package/examples/immeuble/CHECKLIST.md +128 -0
  32. package/examples/immeuble/README.md +121 -0
  33. package/examples/immeuble/bundle/immeuble.bundle.json +22786 -0
  34. package/examples/immeuble/contracts/answer_artifacts.seed.jsonl +34 -0
  35. package/examples/immeuble/contracts/business_capabilities.seed.jsonl +25 -0
  36. package/examples/immeuble/contracts/consumer_contract.yaml +131 -0
  37. package/examples/immeuble/contracts/immeuble_structured_import_model.json +310 -0
  38. package/examples/immeuble/contracts/mapping_external_to_canonical.json +375 -0
  39. package/examples/immeuble/contracts/mapping_external_to_canonical.yaml +55 -0
  40. package/examples/immeuble/contracts/mapping_external_to_canonical_ws.json +65 -0
  41. package/examples/immeuble/contracts/model_contract.json +943 -0
  42. package/examples/immeuble/contracts/projection_catalog.yaml +366 -0
  43. package/examples/immeuble/contracts/scenarios.yaml +27 -0
  44. package/examples/immeuble/contracts/semantic_proposal.golden.json +1 -0
  45. package/examples/immeuble/contracts/source_profile.yaml +38 -0
  46. package/examples/immeuble/fake_data/DeltaFinding.csv +20 -0
  47. package/examples/immeuble/fake_data/ProjectionResult.csv +13 -0
  48. package/examples/immeuble/fake_data/ag_meeting.csv +4 -0
  49. package/examples/immeuble/fake_data/agenda_item.csv +4 -0
  50. package/examples/immeuble/fake_data/architect.csv +3 -0
  51. package/examples/immeuble/fake_data/architecture_firm.csv +2 -0
  52. package/examples/immeuble/fake_data/bank_account.csv +3 -0
  53. package/examples/immeuble/fake_data/billing_group.csv +41 -0
  54. package/examples/immeuble/fake_data/block.csv +10 -0
  55. package/examples/immeuble/fake_data/budget_line.csv +3 -0
  56. package/examples/immeuble/fake_data/building.csv +6 -0
  57. package/examples/immeuble/fake_data/cellar.csv +41 -0
  58. package/examples/immeuble/fake_data/change_order.csv +2 -0
  59. package/examples/immeuble/fake_data/charge_call.csv +58 -0
  60. package/examples/immeuble/fake_data/claim.csv +4 -0
  61. package/examples/immeuble/fake_data/coda_entry.csv +49 -0
  62. package/examples/immeuble/fake_data/compliance_certificate.csv +10 -0
  63. package/examples/immeuble/fake_data/contractor.csv +4 -0
  64. package/examples/immeuble/fake_data/decision.csv +5 -0
  65. package/examples/immeuble/fake_data/defect_reserve.csv +3 -0
  66. package/examples/immeuble/fake_data/household.csv +41 -0
  67. package/examples/immeuble/fake_data/inspection.csv +3 -0
  68. package/examples/immeuble/fake_data/insurance_policy.csv +4 -0
  69. package/examples/immeuble/fake_data/intervention.csv +13 -0
  70. package/examples/immeuble/fake_data/invoice.csv +3 -0
  71. package/examples/immeuble/fake_data/lease_contract.csv +12 -0
  72. package/examples/immeuble/fake_data/maintenance_ticket.csv +13 -0
  73. package/examples/immeuble/fake_data/meter.csv +4 -0
  74. package/examples/immeuble/fake_data/meter_reading.csv +19 -0
  75. package/examples/immeuble/fake_data/milestone.csv +3 -0
  76. package/examples/immeuble/fake_data/organization.csv +12 -0
  77. package/examples/immeuble/fake_data/parking_space.csv +8 -0
  78. package/examples/immeuble/fake_data/permit.csv +3 -0
  79. package/examples/immeuble/fake_data/person.csv +85 -0
  80. package/examples/immeuble/fake_data/private_garden.csv +7 -0
  81. package/examples/immeuble/fake_data/progress_event.csv +2 -0
  82. package/examples/immeuble/fake_data/quote.csv +3 -0
  83. package/examples/immeuble/fake_data/receipt.csv +2 -0
  84. package/examples/immeuble/fake_data/reminder.csv +11 -0
  85. package/examples/immeuble/fake_data/service_contract.csv +4 -0
  86. package/examples/immeuble/fake_data/shared_equipment.csv +8 -0
  87. package/examples/immeuble/fake_data/shared_space.csv +10 -0
  88. package/examples/immeuble/fake_data/unit.csv +41 -0
  89. package/examples/immeuble/fake_data/work_package.csv +4 -0
  90. package/examples/immeuble/fake_data/worksite_project.csv +3 -0
  91. package/examples/immeuble/gap-rules/L0-patrimoine.json +57 -0
  92. package/examples/immeuble/gap-rules/L1-syndic-naive.json +36 -0
  93. package/examples/immeuble/gap-rules/L1-syndic.json +61 -0
  94. package/examples/immeuble/gap-rules/L2-chantier.json +87 -0
  95. package/examples/immeuble/gap-rules/L2-exploitation.json +87 -0
  96. package/examples/immeuble/gap-rules/L2-finance.json +48 -0
  97. package/examples/immeuble/gap-rules/L2-maintenance.json +107 -0
  98. package/examples/immeuble/gap-rules/L2-syndic-filtered.json +39 -0
  99. package/examples/immeuble/gap-rules/L3-full.json +332 -0
  100. package/examples/immeuble/gap-rules/closed-world-contract.md +76 -0
  101. package/examples/immeuble/gap-rules/demo.json +51 -0
  102. package/examples/immeuble/gap-rules/expected-findings.yaml +100 -0
  103. package/examples/immeuble/gap-rules/gap-scenarios.yaml +79 -0
  104. package/examples/immeuble/gap-rules/motifs.json +38 -0
  105. package/examples/immeuble/gap-rules/syndic.json +40 -0
  106. package/examples/immeuble/import_manifest.yaml +25 -0
  107. package/examples/immeuble/import_ready/graph_edges_import.csv +848 -0
  108. package/examples/immeuble/import_ready/mfo_facets_import.csv +559 -0
  109. package/examples/immeuble/index.md +140 -0
  110. package/examples/immeuble/model/immeuble_model.json +418 -0
  111. package/examples/immeuble/reports/01-model.validation.json +56 -0
  112. package/examples/immeuble/reports/02-mapping.validation.json +9 -0
  113. package/examples/immeuble/reports/acceptance.validation.json +161 -0
  114. package/examples/immeuble/reports/consumer_contract.validation.json +162 -0
  115. package/examples/immeuble/reports/graph_edges.jsonl +847 -0
  116. package/examples/immeuble/reports/graph_nodes.jsonl +558 -0
  117. package/examples/immeuble/reports/hybrid-compare.json +144 -0
  118. package/examples/immeuble/reports/immeuble-import-scenario.json +233 -0
  119. package/examples/immeuble/reports/live-artifacts-refresh.validation.json +51 -0
  120. package/examples/immeuble/reports/pipeline_audit.json +59 -0
  121. package/examples/immeuble/reports/projection_audit.json +69 -0
  122. package/examples/immeuble/reports/projection_audit_immeuble.json +257 -0
  123. package/examples/immeuble/reports/projection_audit_immeuble.md +122 -0
  124. package/examples/immeuble/reports/projection_candidates.json +1596 -0
  125. package/examples/immeuble/reports/projection_candidates.md +117 -0
  126. package/examples/immeuble/reports/projection_model_validation.md +115 -0
  127. package/examples/immeuble/reports/reindex.json +4 -0
  128. package/examples/immeuble/reports/reset-immeuble-workspace.json +32 -0
  129. package/examples/immeuble/reports/schema-id-prefix-check.json +5 -0
  130. package/examples/immeuble/scripts/audit-immeuble-projections.mjs +254 -0
  131. package/examples/immeuble/scripts/build-immeuble-model.mjs +308 -0
  132. package/examples/immeuble/scripts/compare-immeuble-snapshots.sh +227 -0
  133. package/examples/immeuble/scripts/enrich-immeuble-demo.mjs +1135 -0
  134. package/examples/immeuble/scripts/reset-immeuble-workspace.mjs +139 -0
  135. package/examples/immeuble/scripts/run-immeuble-backend.sh +164 -0
  136. package/examples/immeuble/scripts/run-immeuble-import.mjs +232 -0
  137. package/examples/immeuble/scripts/run-immeuble-live-lab.sh +439 -0
  138. package/examples/immeuble/scripts/seed-immeuble-gap-rules.mjs +69 -0
  139. package/examples/immeuble/scripts/start-immeuble-demo.sh +52 -0
  140. package/examples/immeuble/scripts/starterkit/analysis-lenses.mjs +181 -0
  141. package/examples/immeuble/scripts/starterkit/analyze-projection-candidates.mjs +714 -0
  142. package/examples/immeuble/scripts/starterkit/audit-ghostcrab-projections.mjs +674 -0
  143. package/examples/immeuble/scripts/starterkit/facet-prefix.mjs +166 -0
  144. package/examples/immeuble/scripts/starterkit/sqlite-utils.mjs +131 -0
  145. package/examples/immeuble/scripts/verify-immeuble-acceptance.mjs +284 -0
  146. package/examples/immeuble/scripts/verify-immeuble-live-artifacts.mjs +140 -0
  147. package/examples/immeuble/scripts/yaml-lite.mjs +96 -0
  148. package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites-immo-mcp.md +161 -0
  149. package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites.md +39 -0
  150. package/examples/immeuble/sources/agent-prompts/prompts/01-discovery-and-model-proposal.md +42 -0
  151. package/examples/immeuble/sources/agent-prompts/prompts/02-ontology-register.md +44 -0
  152. package/examples/immeuble/sources/agent-prompts/prompts/03-gap-rules-design.md +44 -0
  153. package/examples/immeuble/sources/agent-prompts/prompts/04-document-ingest.md +40 -0
  154. package/examples/immeuble/sources/agent-prompts/prompts/05-graph-extraction.md +44 -0
  155. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-immo-mcp.md +109 -0
  156. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-test-immo-mcp3.md +30 -0
  157. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare.md +63 -0
  158. package/examples/immeuble/sources/checklists/gap-rules-checklist.md +42 -0
  159. package/examples/immeuble/sources/checklists/ontology-checklist.md +54 -0
  160. package/examples/immeuble/sources/documents/README.md +42 -0
  161. package/examples/immeuble/sources/documents/annexes-caves-garages-jardins.md +8 -0
  162. package/examples/immeuble/sources/documents/annexes-jardins-garages.md +1 -0
  163. package/examples/immeuble/sources/documents/baux-erables.md +1 -0
  164. package/examples/immeuble/sources/documents/baux-locatifs.md +11 -0
  165. package/examples/immeuble/sources/documents/coda-janvier-2026.md +10 -0
  166. package/examples/immeuble/sources/documents/composition-menages.md +1 -0
  167. package/examples/immeuble/sources/documents/composition-occupants.md +18 -0
  168. package/examples/immeuble/sources/documents/expected-coverage.json +70 -0
  169. package/examples/immeuble/sources/documents/extrait-coda-janvier-2026.md +1 -0
  170. package/examples/immeuble/sources/documents/groupes-facturation.md +30 -0
  171. package/examples/immeuble/sources/documents/manifest.json +79 -0
  172. package/examples/immeuble/sources/documents/note-architecte-chantier-erables.md +3 -0
  173. package/examples/immeuble/sources/documents/permis-urbanisme-erables.md +3 -0
  174. package/examples/immeuble/sources/documents/procedures-operationnelles.md +3 -0
  175. package/examples/immeuble/sources/documents/pv-ag-budget-2026.md +1 -0
  176. package/examples/immeuble/sources/documents/pv-reception-reserves-erables.md +3 -0
  177. package/examples/immeuble/sources/documents/registre-coproprietaires.md +24 -0
  178. package/examples/immeuble/sources/documents/reglement-copropriete-tilleuls.md +1 -0
  179. package/examples/immeuble/sources/documents/statuts-erables.md +22 -0
  180. package/examples/immeuble/sources/documents/statuts-tilleuls.md +19 -0
  181. package/examples/immeuble/sources/documents/succession-jean-dupont.md +3 -0
  182. package/examples/immeuble/sources/documents/titre-propriete-tilleuls-a3.md +1 -0
  183. package/examples/immeuble/sources/documents/trous-pedagogiques.md +3 -0
  184. package/examples/immeuble/sources/ontology/README.md +1 -0
  185. package/examples/immeuble/sources/ontology/core.yaml +444 -0
  186. package/examples/immeuble/success-criteria.yaml +35 -0
  187. package/fixtures/immeuble-demo.sqlite +0 -0
  188. package/package.json +11 -2
  189. package/scripts/lib/sqlite-runtime.mjs +1 -1
  190. package/scripts/load-immeuble-demo.sh +103 -0
  191. package/scripts/seed-immeuble-projections.mjs +75 -148
  192. package/scripts/verify-immeuble-demo-sources.mjs +93 -0
  193. package/scripts/verify-immeuble-demo.mjs +69 -0
  194. package/build/client/_app/immutable/chunks/D0UIlUGZ.js.br +0 -0
  195. package/build/client/_app/immutable/chunks/D0UIlUGZ.js.gz +0 -0
  196. package/build/client/_app/immutable/entry/app.CR-imLox.js.br +0 -0
  197. package/build/client/_app/immutable/entry/app.CR-imLox.js.gz +0 -0
  198. package/build/client/_app/immutable/entry/start.DV-AjeAB.js +0 -1
  199. package/build/client/_app/immutable/entry/start.DV-AjeAB.js.br +0 -2
  200. package/build/client/_app/immutable/entry/start.DV-AjeAB.js.gz +0 -0
  201. package/build/client/_app/immutable/nodes/1.CTEedoSY.js.br +0 -0
  202. package/build/client/_app/immutable/nodes/1.CTEedoSY.js.gz +0 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Facet prefix normalization for entity.facet nomenclature.
3
+ *
4
+ * Stored facets use bare keys in the JSON column; schema context lives in schema_id.
5
+ * Declared required_facets use entity.facet (e.g. unit.quota_basis).
6
+ * This module bridges bare storage and prefixed declarations.
7
+ */
8
+
9
+ /**
10
+ * Derive entity name from schema_id (immeuble:core:unit -> unit) or facets.entity_type.
11
+ * @param {string|null|undefined} schemaId
12
+ * @param {Record<string, unknown>|null} facets
13
+ * @returns {string}
14
+ */
15
+ export function entityFromSchema(schemaId, facets = null) {
16
+ if (schemaId && typeof schemaId === "string") {
17
+ const parts = schemaId.split(":");
18
+ if (parts.length >= 1) {
19
+ return parts.at(-1) ?? "";
20
+ }
21
+ }
22
+ const entityType = facets?.entity_type;
23
+ return typeof entityType === "string" ? entityType : "";
24
+ }
25
+
26
+ /**
27
+ * Build observed facet index from facet rows.
28
+ * Indexes both bare keys and entity.facet prefixed keys.
29
+ *
30
+ * @param {Array<{ schema_id?: string, facets?: string|Record<string, unknown> }>} rows
31
+ * @returns {{ bare: Set<string>, prefixed: Set<string>, all: Set<string> }}
32
+ */
33
+ export function buildObservedFacetIndex(rows) {
34
+ const bare = new Set();
35
+ const prefixed = new Set();
36
+ const all = new Set();
37
+
38
+ for (const row of rows) {
39
+ const facets = parseFacetsJson(row.facets);
40
+ if (!facets || typeof facets !== "object") continue;
41
+
42
+ const entity = entityFromSchema(row.schema_id, facets);
43
+ for (const key of Object.keys(facets)) {
44
+ if (!key || key.startsWith("_")) continue;
45
+ bare.add(key);
46
+ all.add(key);
47
+ if (entity) {
48
+ const prefixedKey = `${entity}.${key}`;
49
+ prefixed.add(prefixedKey);
50
+ all.add(prefixedKey);
51
+ }
52
+ }
53
+ }
54
+
55
+ return { bare, prefixed, all };
56
+ }
57
+
58
+ /**
59
+ * Check whether a required facet is observed (prefix-aware).
60
+ * - entity.facet -> match prefixed index (or bare if entity matches and bare exists)
61
+ * - bare facet -> match bare index only
62
+ *
63
+ * @param {string} requiredFacet
64
+ * @param {{ bare: Set<string>, prefixed: Set<string>, all: Set<string> }} index
65
+ * @returns {boolean}
66
+ */
67
+ export function facetIsObserved(requiredFacet, index) {
68
+ if (!requiredFacet) return false;
69
+ if (requiredFacet.includes(".")) {
70
+ if (index.prefixed.has(requiredFacet) || index.all.has(requiredFacet)) {
71
+ return true;
72
+ }
73
+ const dot = requiredFacet.indexOf(".");
74
+ const entity = requiredFacet.slice(0, dot);
75
+ const bareKey = requiredFacet.slice(dot + 1);
76
+ return index.bare.has(bareKey) && Boolean(entity);
77
+ }
78
+ return index.bare.has(requiredFacet) || index.all.has(requiredFacet);
79
+ }
80
+
81
+ /**
82
+ * Return required facets not observed in the index.
83
+ * @param {string[]} requiredFacets
84
+ * @param {{ bare: Set<string>, prefixed: Set<string>, all: Set<string> }} index
85
+ * @returns {string[]}
86
+ */
87
+ export function missingRequiredFacets(requiredFacets, index) {
88
+ return [...new Set(requiredFacets)].filter((f) => !facetIsObserved(f, index)).sort();
89
+ }
90
+
91
+ /**
92
+ * Extract known facet terms from a StarterKit model_contract.json shape.
93
+ * @param {Record<string, unknown>} contract
94
+ * @returns {{ schemas: Set<string>, facets: Set<string>, edges: Set<string> }}
95
+ */
96
+ export function knownTermsFromContract(contract) {
97
+ const schemas = new Set(Object.keys(contract.schemas ?? {}));
98
+ const facets = new Set(["record_id", "workspace_id", "label"]);
99
+ for (const schema of Object.values(contract.schemas ?? {})) {
100
+ if (schema && typeof schema === "object" && schema.facets) {
101
+ for (const key of Object.keys(schema.facets)) {
102
+ facets.add(key);
103
+ }
104
+ }
105
+ }
106
+ const edges = new Set();
107
+ for (const edge of contract.edge_types ?? []) {
108
+ const type = typeof edge === "string" ? edge : edge?.type;
109
+ if (type) edges.add(type);
110
+ }
111
+ return { schemas, facets, edges };
112
+ }
113
+
114
+ /**
115
+ * Normalize edge type for comparison (case-insensitive).
116
+ * @param {string} edge
117
+ * @returns {string}
118
+ */
119
+ export function normalizeEdgeType(edge) {
120
+ return String(edge ?? "").trim().toUpperCase();
121
+ }
122
+
123
+ /**
124
+ * Check if required edge exists in relation counts (case-insensitive).
125
+ * @param {string} requiredEdge
126
+ * @param {Record<string, number>} relationCounts
127
+ * @returns {boolean}
128
+ */
129
+ export function edgeIsObserved(requiredEdge, relationCounts) {
130
+ const norm = normalizeEdgeType(requiredEdge);
131
+ for (const [key, count] of Object.entries(relationCounts)) {
132
+ if (normalizeEdgeType(key) === norm && count > 0) {
133
+ return true;
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * @param {unknown} value
141
+ * @returns {Record<string, unknown>|null}
142
+ */
143
+ export function parseFacetsJson(value) {
144
+ if (value == null) return null;
145
+ if (typeof value === "object" && !Array.isArray(value)) {
146
+ return /** @type {Record<string, unknown>} */ (value);
147
+ }
148
+ if (typeof value !== "string") return null;
149
+ try {
150
+ const parsed = JSON.parse(value);
151
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
152
+ ? /** @type {Record<string, unknown>} */ (parsed)
153
+ : null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Format facet list for markdown output.
161
+ * @param {string[]} values
162
+ * @returns {string}
163
+ */
164
+ export function fmtFacetValues(values) {
165
+ return values.length ? values.map((v) => `\`${v}\``).join(", ") : "n/a";
166
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * SQLite helpers for StarterKit projection scripts (CLI sqlite3, JSON output).
3
+ */
4
+
5
+ import { existsSync } from "node:fs";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ /**
9
+ * @param {string} dbPath
10
+ * @param {string} sql
11
+ * @returns {Record<string, unknown>[]}
12
+ */
13
+ export function sqliteQuery(dbPath, sql) {
14
+ if (!existsSync(dbPath)) {
15
+ throw new Error(`SQLite database not found: ${dbPath}`);
16
+ }
17
+ const res = spawnSync("sqlite3", [dbPath, "-json", sql], { encoding: "utf8" });
18
+ if (res.status !== 0) {
19
+ throw new Error(res.stderr?.trim() || res.stdout?.trim() || "sqlite3 query failed");
20
+ }
21
+ const text = (res.stdout || "").trim();
22
+ if (!text) return [];
23
+ try {
24
+ const parsed = JSON.parse(text);
25
+ return Array.isArray(parsed) ? parsed : [];
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string} dbPath
33
+ * @param {string} table
34
+ * @returns {boolean}
35
+ */
36
+ export function sqliteTableExists(dbPath, table) {
37
+ const rows = sqliteQuery(
38
+ dbPath,
39
+ `SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name='${table.replace(/'/g, "''")}' LIMIT 1`
40
+ );
41
+ return rows.length > 0;
42
+ }
43
+
44
+ /**
45
+ * @param {string} dbPath
46
+ * @param {string} table
47
+ * @returns {Set<string>}
48
+ */
49
+ export function sqliteTableColumns(dbPath, table) {
50
+ if (!sqliteTableExists(dbPath, table)) return new Set();
51
+ const rows = sqliteQuery(dbPath, `PRAGMA table_info('${table.replace(/'/g, "''")}')`);
52
+ return new Set(rows.map((r) => String(r.name ?? "")).filter(Boolean));
53
+ }
54
+
55
+ /**
56
+ * @param {Set<string>} columns
57
+ * @param {string|null|undefined} workspaceId
58
+ * @returns {{ where: string, params: string[] }}
59
+ */
60
+ export function workspaceWhere(columns, workspaceId) {
61
+ if (workspaceId && columns.has("workspace_id")) {
62
+ return {
63
+ where: `WHERE workspace_id = '${escapeSql(workspaceId)}'`,
64
+ params: [workspaceId]
65
+ };
66
+ }
67
+ return { where: "", params: [] };
68
+ }
69
+
70
+ /**
71
+ * @param {string} value
72
+ * @returns {string}
73
+ */
74
+ export function escapeSql(value) {
75
+ return String(value).replace(/'/g, "''");
76
+ }
77
+
78
+ /**
79
+ * @param {unknown} value
80
+ * @returns {[boolean, unknown]}
81
+ */
82
+ export function parseJsonMaybe(value) {
83
+ if (value == null) return [false, null];
84
+ if (typeof value === "object") return [true, value];
85
+ try {
86
+ return [true, JSON.parse(String(value))];
87
+ } catch {
88
+ return [false, null];
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {number|null|undefined} unix
94
+ * @returns {string|null}
95
+ */
96
+ export function dtFromUnix(unix) {
97
+ if (!unix) return null;
98
+ return new Date(Number(unix) * 1000).toISOString();
99
+ }
100
+
101
+ /**
102
+ * @param {Record<string, string>} argvMap
103
+ * @param {string} name
104
+ * @param {string} defaultValue
105
+ * @returns {string}
106
+ */
107
+ export function parseFlag(argvMap, name, defaultValue = "") {
108
+ return argvMap[name] ?? defaultValue;
109
+ }
110
+
111
+ /**
112
+ * @param {string[]} argv
113
+ * @returns {Record<string, string>}
114
+ */
115
+ export function parseArgs(argv) {
116
+ /** @type {Record<string, string>} */
117
+ const out = {};
118
+ for (let i = 0; i < argv.length; i++) {
119
+ const arg = argv[i];
120
+ if (!arg.startsWith("--")) continue;
121
+ const key = arg.slice(2);
122
+ const next = argv[i + 1];
123
+ if (next && !next.startsWith("--")) {
124
+ out[key] = next;
125
+ i++;
126
+ } else {
127
+ out[key] = "true";
128
+ }
129
+ }
130
+ return out;
131
+ }
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify immeuble ACCEPTANCE.yaml against filesystem reports and optional DB.
4
+ *
5
+ * Usage:
6
+ * node examples/immeuble/scripts/verify-immeuble-acceptance.mjs [--db path] [--require-hybrid]
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { spawnSync } from "node:child_process";
13
+ import { parseYaml } from "./yaml-lite.mjs";
14
+
15
+ const immeubleRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ const pkgRoot = resolve(immeubleRoot, "..", "..");
17
+ const reportsDir = join(immeubleRoot, "reports");
18
+
19
+ const args = process.argv.slice(2);
20
+ const dbPath = parseFlag(args, "--db", process.env.GHOSTCRAB_SQLITE_PATH ?? "");
21
+ const requireHybrid = args.includes("--require-hybrid");
22
+ const requireBundle = args.includes("--require-bundle");
23
+ const projectionStrict = args.includes("--projection-strict");
24
+ const requireBusinessCapabilities = args.includes("--require-business-capabilities");
25
+
26
+ const acceptance = parseYaml(readFileSync(join(immeubleRoot, "ACCEPTANCE.yaml"), "utf8"));
27
+ const results = { ok: true, checks: [], workspace_id: acceptance.workspace_id };
28
+
29
+ function check(name, ok, detail) {
30
+ results.checks.push({ name, ok, detail });
31
+ if (!ok) results.ok = false;
32
+ }
33
+
34
+ function expectedMin(section, exactKey, minKey) {
35
+ return Number(section?.[minKey] ?? section?.[exactKey] ?? 0);
36
+ }
37
+
38
+ function readJson(rel) {
39
+ const path = join(immeubleRoot, rel);
40
+ if (!existsSync(path)) return null;
41
+ return JSON.parse(readFileSync(path, "utf8"));
42
+ }
43
+
44
+ const pipeline = readJson("reports/pipeline_audit.json");
45
+ check("pipeline_audit exists", pipeline != null, pipeline ? "ok" : "missing reports/pipeline_audit.json");
46
+ if (pipeline) {
47
+ check("pipeline_audit.ok", pipeline.ok === true, String(pipeline.ok));
48
+ check(
49
+ "facet_rows",
50
+ pipeline.facet_rows >= expectedMin(acceptance.import_ready, "facet_rows", "facet_rows_min"),
51
+ `${pipeline.facet_rows} >= ${expectedMin(acceptance.import_ready, "facet_rows", "facet_rows_min")}`
52
+ );
53
+ check(
54
+ "edge_rows",
55
+ pipeline.edge_rows >= expectedMin(acceptance.import_ready, "edge_rows", "edge_rows_min"),
56
+ `${pipeline.edge_rows} >= ${expectedMin(acceptance.import_ready, "edge_rows", "edge_rows_min")}`
57
+ );
58
+ const typeCounts = acceptance.import_ready.entity_type_counts_min ?? acceptance.import_ready.entity_type_counts ?? {};
59
+ for (const [type, expected] of Object.entries(typeCounts)) {
60
+ const actual = pipeline.counts?.[type];
61
+ check(`entity_count.${type}`, actual >= expected, `${actual} >= ${expected}`);
62
+ }
63
+ }
64
+
65
+ const prefixReport = readJson("reports/01-model.validation.json");
66
+ if (prefixReport) {
67
+ check("schema_id_prefix", prefixReport.ok === true && prefixReport.prefix_violations === 0, JSON.stringify(prefixReport));
68
+ }
69
+
70
+ const projectionCatalogPath = join(immeubleRoot, "contracts/projection_catalog.yaml");
71
+ if (existsSync(projectionCatalogPath)) {
72
+ const projectionCatalog = parseYaml(readFileSync(projectionCatalogPath, "utf8"));
73
+ const projections = Array.isArray(projectionCatalog.projections) ? projectionCatalog.projections : [];
74
+ const mismatches = projections
75
+ .filter((entry) => String(entry.description ?? "").trim() !== String(entry.business_question ?? "").trim())
76
+ .map((entry) => entry.name)
77
+ .filter(Boolean);
78
+ check(
79
+ "projection_catalog.description_is_question",
80
+ projections.length > 0 && mismatches.length === 0,
81
+ mismatches.length === 0 ? `${projections.length} projections` : `mismatches: ${mismatches.join(", ")}`
82
+ );
83
+ } else {
84
+ check("projection_catalog file", false, "missing contracts/projection_catalog.yaml");
85
+ }
86
+
87
+ const hybridPath = join(reportsDir, "hybrid-compare.json");
88
+ if (existsSync(hybridPath)) {
89
+ const hybrid = JSON.parse(readFileSync(hybridPath, "utf8"));
90
+ const deltas = hybrid.compare?.deltas ?? {};
91
+ for (const [key, max] of Object.entries(acceptance.hybrid_compare.max_deltas)) {
92
+ const delta = deltas[key]?.delta;
93
+ if (typeof delta === "number") {
94
+ check(`hybrid_delta.${key}`, Math.abs(delta) <= max, `delta=${delta}`);
95
+ }
96
+ }
97
+ } else if (requireHybrid || acceptance.hybrid_compare.required) {
98
+ check("hybrid_compare report", false, "missing reports/hybrid-compare.json");
99
+ }
100
+
101
+ if (dbPath && existsSync(dbPath)) {
102
+ const ws = acceptance.workspace_id;
103
+ const q = (sql) => {
104
+ const res = spawnSync("sqlite3", [dbPath, "-json", sql], { encoding: "utf8" });
105
+ if (res.status !== 0) return null;
106
+ try { return JSON.parse(res.stdout || "[]"); } catch { return []; }
107
+ };
108
+
109
+ const facts = q(`SELECT COUNT(*) AS count FROM agent_facts WHERE workspace_id='${ws}'`);
110
+ const factsCount = Number(facts?.[0]?.count ?? 0);
111
+ const rawEntitiesForFacts = q(`SELECT COUNT(*) AS count FROM entities_raw WHERE workspace_id='${ws}'`);
112
+ const factsOrRawCount = Math.max(factsCount, Number(rawEntitiesForFacts?.[0]?.count ?? 0));
113
+ check(
114
+ "db.agent_facts_or_raw_entities",
115
+ factsOrRawCount >= acceptance.db_after_import.agent_facts_min,
116
+ `${factsOrRawCount} >= ${acceptance.db_after_import.agent_facts_min}`
117
+ );
118
+
119
+ const violations = q(
120
+ `SELECT schema_id FROM agent_facts WHERE workspace_id='${ws}' AND schema_id NOT LIKE '${acceptance.schema_id_prefix}%' LIMIT 5`
121
+ );
122
+ check(
123
+ "db.schema_id_violations",
124
+ (violations?.length ?? 0) === acceptance.db_after_import.schema_id_violations,
125
+ JSON.stringify(violations?.map((r) => r.schema_id) ?? [])
126
+ );
127
+
128
+ const rels = q(`SELECT COUNT(*) AS count FROM relations_raw WHERE workspace_id='${ws}'`);
129
+ check(
130
+ "db.relations_raw",
131
+ Number(rels?.[0]?.count ?? 0) >= acceptance.db_after_import.relations_raw_min,
132
+ String(rels?.[0]?.count)
133
+ );
134
+
135
+ const graph = q(`SELECT COUNT(*) AS count FROM graph_entity WHERE workspace_id='${ws}'`);
136
+ check(
137
+ "db.graph_entity",
138
+ Number(graph?.[0]?.count ?? 0) >= acceptance.db_after_import.graph_entity_min,
139
+ String(graph?.[0]?.count)
140
+ );
141
+
142
+ const gapRulesMin = Number(acceptance.construction_lite?.gap_rules_min ?? 0);
143
+ if (gapRulesMin > 0) {
144
+ const gapRules = q(`SELECT COUNT(*) AS count FROM graph_gap_rules WHERE workspace_id='${ws}'`);
145
+ check(
146
+ "db.graph_gap_rules",
147
+ Number(gapRules?.[0]?.count ?? 0) >= gapRulesMin,
148
+ `${Number(gapRules?.[0]?.count ?? 0)} >= ${gapRulesMin}`
149
+ );
150
+ }
151
+
152
+ if (requireBusinessCapabilities) {
153
+ const businessCapabilities = acceptance.business_capabilities ?? {};
154
+ const activeMin = Number(businessCapabilities.active_min ?? 1);
155
+ const active = q(
156
+ `SELECT COUNT(*) AS count FROM agent_facts WHERE workspace_id='${ws}' AND schema_id='ghostcrab:business-capability' AND json_extract(facets_json, '$.activation_status') = 'active'`
157
+ );
158
+ const activeCount = Number(active?.[0]?.count ?? 0);
159
+ check(
160
+ "db.business_capability_active_count",
161
+ activeCount >= activeMin,
162
+ `${activeCount} >= ${activeMin}`
163
+ );
164
+
165
+ const requiredArtifacts = Array.isArray(businessCapabilities.required_artifact_ids)
166
+ ? businessCapabilities.required_artifact_ids.map((value) => String(value)).filter(Boolean)
167
+ : [];
168
+ if (requiredArtifacts.length > 0) {
169
+ const rows = q(
170
+ `SELECT json_extract(facets_json, '$.artifact_id') AS artifact_id FROM agent_facts WHERE workspace_id='${ws}' AND schema_id='ghostcrab:business-capability' AND json_extract(facets_json, '$.activation_status') = 'active' AND json_extract(facets_json, '$.artifact_id') IS NOT NULL`
171
+ );
172
+ const present = new Set((rows ?? []).map((row) => String(row.artifact_id)));
173
+ for (const artifactId of requiredArtifacts) {
174
+ check(
175
+ `db.business_capability_artifact_id.${artifactId}`,
176
+ present.has(artifactId),
177
+ present.has(artifactId) ? "present" : "missing"
178
+ );
179
+ }
180
+ }
181
+ }
182
+ } else {
183
+ results.checks.push({ name: "db checks", ok: true, detail: "skipped (no --db)" });
184
+ }
185
+
186
+ const starterkitAudit = acceptance.starterkit_projection_audit;
187
+ const auditReportPath = join(reportsDir, `projection_audit_${acceptance.workspace_id}.json`);
188
+ if (starterkitAudit && existsSync(auditReportPath)) {
189
+ const auditReport = JSON.parse(readFileSync(auditReportPath, "utf8"));
190
+ const summary = auditReport.summary ?? {};
191
+ const enforce = !starterkitAudit.strict_only || projectionStrict;
192
+
193
+ if (enforce) {
194
+ check(
195
+ "starterkit.quality_score",
196
+ Number(summary.quality_score ?? 0) >= Number(starterkitAudit.quality_score_min ?? 0),
197
+ `${summary.quality_score} >= ${starterkitAudit.quality_score_min}`
198
+ );
199
+ check(
200
+ "starterkit.facet_gaps",
201
+ Number(summary.required_facet_observation_gap_count ?? 0) <= Number(starterkitAudit.required_facet_observation_gap_max ?? 0),
202
+ String(summary.required_facet_observation_gap_count)
203
+ );
204
+ check(
205
+ "starterkit.schema_gaps",
206
+ Number(summary.required_schema_record_gap_count ?? 0) <= Number(starterkitAudit.required_schema_record_gap_max ?? 0),
207
+ String(summary.required_schema_record_gap_count)
208
+ );
209
+ check(
210
+ "starterkit.edge_gaps",
211
+ Number(summary.required_edge_type_gap_count ?? 0) <= Number(starterkitAudit.required_edge_type_gap_max ?? 0),
212
+ String(summary.required_edge_type_gap_count)
213
+ );
214
+ check(
215
+ "starterkit.planned_missing",
216
+ Number(summary.planned_missing_count ?? 0) <= Number(starterkitAudit.planned_missing_count_max ?? 0),
217
+ String(summary.planned_missing_count)
218
+ );
219
+ } else {
220
+ results.checks.push({
221
+ name: "starterkit_projection_audit",
222
+ ok: true,
223
+ detail: "informational (use --projection-strict to enforce)"
224
+ });
225
+ }
226
+ } else if (starterkitAudit && projectionStrict) {
227
+ check("starterkit_projection_audit report", false, `missing ${auditReportPath}`);
228
+ }
229
+
230
+ if (requireBundle) {
231
+ const bundlePath = join(immeubleRoot, acceptance.bundle.path);
232
+ if (existsSync(bundlePath)) {
233
+ const bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
234
+ check("bundle.scope", bundle.scope?.workspace_id === acceptance.workspace_id, bundle.scope?.workspace_id);
235
+ check(
236
+ "bundle.documents_raw",
237
+ (bundle.documents_raw?.length ?? 0) >= expectedMin(acceptance.bundle, "documents_raw", "documents_raw_min"),
238
+ String(bundle.documents_raw?.length)
239
+ );
240
+ check(
241
+ "bundle.entities_raw",
242
+ (bundle.entities_raw?.length ?? 0) >= acceptance.bundle.entities_raw_min,
243
+ String(bundle.entities_raw?.length)
244
+ );
245
+ check(
246
+ "bundle.relations_raw",
247
+ (bundle.relations_raw?.length ?? 0) >= acceptance.bundle.relations_raw_min,
248
+ String(bundle.relations_raw?.length)
249
+ );
250
+ check(
251
+ "bundle.answer_artifacts",
252
+ (bundle.mindbrain_answer_artifacts?.length ?? 0) >= acceptance.bundle.answer_artifacts_min,
253
+ String(bundle.mindbrain_answer_artifacts?.length)
254
+ );
255
+ } else {
256
+ check("bundle file", false, acceptance.bundle.path);
257
+ }
258
+ }
259
+
260
+ if (acceptance.expected_findings?.file) {
261
+ const findingsPath = join(immeubleRoot, acceptance.expected_findings.file);
262
+ if (existsSync(findingsPath)) {
263
+ const findings = parseYaml(readFileSync(findingsPath, "utf8"));
264
+ const count = Array.isArray(findings.expected_findings) ? findings.expected_findings.length : 0;
265
+ check(
266
+ "expected_findings",
267
+ count >= Number(acceptance.expected_findings.min_count ?? 1),
268
+ `${count} >= ${acceptance.expected_findings.min_count}`
269
+ );
270
+ } else {
271
+ check("expected_findings file", false, acceptance.expected_findings.file);
272
+ }
273
+ }
274
+
275
+ mkdirSync(reportsDir, { recursive: true });
276
+ writeFileSync(join(reportsDir, "acceptance.validation.json"), JSON.stringify(results, null, 2) + "\n", "utf8");
277
+ console.log(JSON.stringify(results, null, 2));
278
+ process.exit(results.ok ? 0 : 1);
279
+
280
+ function parseFlag(argv, name, defaultValue) {
281
+ const index = argv.indexOf(name);
282
+ if (index === -1) return defaultValue;
283
+ return argv[index + 1] ?? defaultValue;
284
+ }