@mindflight/mindbrain-personal-studio 0.6.1 → 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 (212) hide show
  1. package/README.md +16 -8
  2. package/build/client/_app/immutable/chunks/CIErFlYG.js +1 -0
  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.CVz6aYsT.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.BBtxY46Q.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-D8EA_he2.js → internal.js-C3tV0XXj.js} +2 -2
  20. package/build/server/chunks/chunks/{internal.js-D8EA_he2.js.map → internal.js-C3tV0XXj.js.map} +1 -1
  21. package/build/server/chunks/entries/endpoints/api/graph/schema-registry/{_server.ts.js-Dyfsc-VA.js → _server.ts.js-CAsXxBRq.js} +7 -2
  22. package/build/server/chunks/entries/endpoints/api/graph/schema-registry/_server.ts.js-CAsXxBRq.js.map +1 -0
  23. package/build/server/chunks/{handler-BjXBCd1c.js → handler-DlaCCnxx.js} +3 -3
  24. package/build/server/chunks/{handler-BjXBCd1c.js.map → handler-DlaCCnxx.js.map} +1 -1
  25. package/build/server/chunks/{index.js-BJYcV8V7.js → index.js-BGfKWHak.js} +2 -2
  26. package/build/server/chunks/{index.js-BJYcV8V7.js.map → index.js-BGfKWHak.js.map} +1 -1
  27. package/build/server/chunks/{manifest.js-Ddaot0P4.js → manifest.js-CnmaNf5D.js} +4 -4
  28. package/build/server/chunks/{manifest.js-Ddaot0P4.js.map → manifest.js-CnmaNf5D.js.map} +1 -1
  29. package/build/server/chunks/nodes/{1.js-BRigw_9J.js → 1.js-BypjwBYB.js} +2 -2
  30. package/build/server/chunks/nodes/{1.js-BRigw_9J.js.map → 1.js-BypjwBYB.js.map} +1 -1
  31. package/examples/immeuble/ACCEPTANCE.yaml +82 -0
  32. package/examples/immeuble/BIM_LITE.md +15 -0
  33. package/examples/immeuble/CHECKLIST.md +128 -0
  34. package/examples/immeuble/README.md +121 -0
  35. package/examples/immeuble/bundle/immeuble.bundle.json +22786 -0
  36. package/examples/immeuble/contracts/answer_artifacts.seed.jsonl +34 -0
  37. package/examples/immeuble/contracts/business_capabilities.seed.jsonl +25 -0
  38. package/examples/immeuble/contracts/consumer_contract.yaml +131 -0
  39. package/examples/immeuble/contracts/immeuble_structured_import_model.json +310 -0
  40. package/examples/immeuble/contracts/mapping_external_to_canonical.json +375 -0
  41. package/examples/immeuble/contracts/mapping_external_to_canonical.yaml +55 -0
  42. package/examples/immeuble/contracts/mapping_external_to_canonical_ws.json +65 -0
  43. package/examples/immeuble/contracts/model_contract.json +943 -0
  44. package/examples/immeuble/contracts/projection_catalog.yaml +366 -0
  45. package/examples/immeuble/contracts/scenarios.yaml +27 -0
  46. package/examples/immeuble/contracts/semantic_proposal.golden.json +1 -0
  47. package/examples/immeuble/contracts/source_profile.yaml +38 -0
  48. package/examples/immeuble/fake_data/DeltaFinding.csv +20 -0
  49. package/examples/immeuble/fake_data/ProjectionResult.csv +13 -0
  50. package/examples/immeuble/fake_data/ag_meeting.csv +4 -0
  51. package/examples/immeuble/fake_data/agenda_item.csv +4 -0
  52. package/examples/immeuble/fake_data/architect.csv +3 -0
  53. package/examples/immeuble/fake_data/architecture_firm.csv +2 -0
  54. package/examples/immeuble/fake_data/bank_account.csv +3 -0
  55. package/examples/immeuble/fake_data/billing_group.csv +41 -0
  56. package/examples/immeuble/fake_data/block.csv +10 -0
  57. package/examples/immeuble/fake_data/budget_line.csv +3 -0
  58. package/examples/immeuble/fake_data/building.csv +6 -0
  59. package/examples/immeuble/fake_data/cellar.csv +41 -0
  60. package/examples/immeuble/fake_data/change_order.csv +2 -0
  61. package/examples/immeuble/fake_data/charge_call.csv +58 -0
  62. package/examples/immeuble/fake_data/claim.csv +4 -0
  63. package/examples/immeuble/fake_data/coda_entry.csv +49 -0
  64. package/examples/immeuble/fake_data/compliance_certificate.csv +10 -0
  65. package/examples/immeuble/fake_data/contractor.csv +4 -0
  66. package/examples/immeuble/fake_data/decision.csv +5 -0
  67. package/examples/immeuble/fake_data/defect_reserve.csv +3 -0
  68. package/examples/immeuble/fake_data/household.csv +41 -0
  69. package/examples/immeuble/fake_data/inspection.csv +3 -0
  70. package/examples/immeuble/fake_data/insurance_policy.csv +4 -0
  71. package/examples/immeuble/fake_data/intervention.csv +13 -0
  72. package/examples/immeuble/fake_data/invoice.csv +3 -0
  73. package/examples/immeuble/fake_data/lease_contract.csv +12 -0
  74. package/examples/immeuble/fake_data/maintenance_ticket.csv +13 -0
  75. package/examples/immeuble/fake_data/meter.csv +4 -0
  76. package/examples/immeuble/fake_data/meter_reading.csv +19 -0
  77. package/examples/immeuble/fake_data/milestone.csv +3 -0
  78. package/examples/immeuble/fake_data/organization.csv +12 -0
  79. package/examples/immeuble/fake_data/parking_space.csv +8 -0
  80. package/examples/immeuble/fake_data/permit.csv +3 -0
  81. package/examples/immeuble/fake_data/person.csv +85 -0
  82. package/examples/immeuble/fake_data/private_garden.csv +7 -0
  83. package/examples/immeuble/fake_data/progress_event.csv +2 -0
  84. package/examples/immeuble/fake_data/quote.csv +3 -0
  85. package/examples/immeuble/fake_data/receipt.csv +2 -0
  86. package/examples/immeuble/fake_data/reminder.csv +11 -0
  87. package/examples/immeuble/fake_data/service_contract.csv +4 -0
  88. package/examples/immeuble/fake_data/shared_equipment.csv +8 -0
  89. package/examples/immeuble/fake_data/shared_space.csv +10 -0
  90. package/examples/immeuble/fake_data/unit.csv +41 -0
  91. package/examples/immeuble/fake_data/work_package.csv +4 -0
  92. package/examples/immeuble/fake_data/worksite_project.csv +3 -0
  93. package/examples/immeuble/gap-rules/L0-patrimoine.json +57 -0
  94. package/examples/immeuble/gap-rules/L1-syndic-naive.json +36 -0
  95. package/examples/immeuble/gap-rules/L1-syndic.json +61 -0
  96. package/examples/immeuble/gap-rules/L2-chantier.json +87 -0
  97. package/examples/immeuble/gap-rules/L2-exploitation.json +87 -0
  98. package/examples/immeuble/gap-rules/L2-finance.json +48 -0
  99. package/examples/immeuble/gap-rules/L2-maintenance.json +107 -0
  100. package/examples/immeuble/gap-rules/L2-syndic-filtered.json +39 -0
  101. package/examples/immeuble/gap-rules/L3-full.json +332 -0
  102. package/examples/immeuble/gap-rules/closed-world-contract.md +76 -0
  103. package/examples/immeuble/gap-rules/demo.json +51 -0
  104. package/examples/immeuble/gap-rules/expected-findings.yaml +100 -0
  105. package/examples/immeuble/gap-rules/gap-scenarios.yaml +79 -0
  106. package/examples/immeuble/gap-rules/motifs.json +38 -0
  107. package/examples/immeuble/gap-rules/syndic.json +40 -0
  108. package/examples/immeuble/import_manifest.yaml +25 -0
  109. package/examples/immeuble/import_ready/graph_edges_import.csv +848 -0
  110. package/examples/immeuble/import_ready/mfo_facets_import.csv +559 -0
  111. package/examples/immeuble/index.md +140 -0
  112. package/examples/immeuble/model/immeuble_model.json +418 -0
  113. package/examples/immeuble/reports/01-model.validation.json +56 -0
  114. package/examples/immeuble/reports/02-mapping.validation.json +9 -0
  115. package/examples/immeuble/reports/acceptance.validation.json +161 -0
  116. package/examples/immeuble/reports/consumer_contract.validation.json +162 -0
  117. package/examples/immeuble/reports/graph_edges.jsonl +847 -0
  118. package/examples/immeuble/reports/graph_nodes.jsonl +558 -0
  119. package/examples/immeuble/reports/hybrid-compare.json +144 -0
  120. package/examples/immeuble/reports/immeuble-import-scenario.json +233 -0
  121. package/examples/immeuble/reports/live-artifacts-refresh.validation.json +51 -0
  122. package/examples/immeuble/reports/pipeline_audit.json +59 -0
  123. package/examples/immeuble/reports/projection_audit.json +69 -0
  124. package/examples/immeuble/reports/projection_audit_immeuble.json +257 -0
  125. package/examples/immeuble/reports/projection_audit_immeuble.md +122 -0
  126. package/examples/immeuble/reports/projection_candidates.json +1596 -0
  127. package/examples/immeuble/reports/projection_candidates.md +117 -0
  128. package/examples/immeuble/reports/projection_model_validation.md +115 -0
  129. package/examples/immeuble/reports/reindex.json +4 -0
  130. package/examples/immeuble/reports/reset-immeuble-workspace.json +32 -0
  131. package/examples/immeuble/reports/schema-id-prefix-check.json +5 -0
  132. package/examples/immeuble/scripts/audit-immeuble-projections.mjs +254 -0
  133. package/examples/immeuble/scripts/build-immeuble-model.mjs +308 -0
  134. package/examples/immeuble/scripts/compare-immeuble-snapshots.sh +227 -0
  135. package/examples/immeuble/scripts/enrich-immeuble-demo.mjs +1135 -0
  136. package/examples/immeuble/scripts/reset-immeuble-workspace.mjs +139 -0
  137. package/examples/immeuble/scripts/run-immeuble-backend.sh +164 -0
  138. package/examples/immeuble/scripts/run-immeuble-import.mjs +232 -0
  139. package/examples/immeuble/scripts/run-immeuble-live-lab.sh +439 -0
  140. package/examples/immeuble/scripts/seed-immeuble-gap-rules.mjs +69 -0
  141. package/examples/immeuble/scripts/start-immeuble-demo.sh +52 -0
  142. package/examples/immeuble/scripts/starterkit/analysis-lenses.mjs +181 -0
  143. package/examples/immeuble/scripts/starterkit/analyze-projection-candidates.mjs +714 -0
  144. package/examples/immeuble/scripts/starterkit/audit-ghostcrab-projections.mjs +674 -0
  145. package/examples/immeuble/scripts/starterkit/facet-prefix.mjs +166 -0
  146. package/examples/immeuble/scripts/starterkit/sqlite-utils.mjs +131 -0
  147. package/examples/immeuble/scripts/verify-immeuble-acceptance.mjs +284 -0
  148. package/examples/immeuble/scripts/verify-immeuble-live-artifacts.mjs +140 -0
  149. package/examples/immeuble/scripts/yaml-lite.mjs +96 -0
  150. package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites-immo-mcp.md +161 -0
  151. package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites.md +39 -0
  152. package/examples/immeuble/sources/agent-prompts/prompts/01-discovery-and-model-proposal.md +42 -0
  153. package/examples/immeuble/sources/agent-prompts/prompts/02-ontology-register.md +44 -0
  154. package/examples/immeuble/sources/agent-prompts/prompts/03-gap-rules-design.md +44 -0
  155. package/examples/immeuble/sources/agent-prompts/prompts/04-document-ingest.md +40 -0
  156. package/examples/immeuble/sources/agent-prompts/prompts/05-graph-extraction.md +44 -0
  157. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-immo-mcp.md +109 -0
  158. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-test-immo-mcp3.md +30 -0
  159. package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare.md +63 -0
  160. package/examples/immeuble/sources/checklists/gap-rules-checklist.md +42 -0
  161. package/examples/immeuble/sources/checklists/ontology-checklist.md +54 -0
  162. package/examples/immeuble/sources/documents/README.md +42 -0
  163. package/examples/immeuble/sources/documents/annexes-caves-garages-jardins.md +8 -0
  164. package/examples/immeuble/sources/documents/annexes-jardins-garages.md +1 -0
  165. package/examples/immeuble/sources/documents/baux-erables.md +1 -0
  166. package/examples/immeuble/sources/documents/baux-locatifs.md +11 -0
  167. package/examples/immeuble/sources/documents/coda-janvier-2026.md +10 -0
  168. package/examples/immeuble/sources/documents/composition-menages.md +1 -0
  169. package/examples/immeuble/sources/documents/composition-occupants.md +18 -0
  170. package/examples/immeuble/sources/documents/expected-coverage.json +70 -0
  171. package/examples/immeuble/sources/documents/extrait-coda-janvier-2026.md +1 -0
  172. package/examples/immeuble/sources/documents/groupes-facturation.md +30 -0
  173. package/examples/immeuble/sources/documents/manifest.json +79 -0
  174. package/examples/immeuble/sources/documents/note-architecte-chantier-erables.md +3 -0
  175. package/examples/immeuble/sources/documents/permis-urbanisme-erables.md +3 -0
  176. package/examples/immeuble/sources/documents/procedures-operationnelles.md +3 -0
  177. package/examples/immeuble/sources/documents/pv-ag-budget-2026.md +1 -0
  178. package/examples/immeuble/sources/documents/pv-reception-reserves-erables.md +3 -0
  179. package/examples/immeuble/sources/documents/registre-coproprietaires.md +24 -0
  180. package/examples/immeuble/sources/documents/reglement-copropriete-tilleuls.md +1 -0
  181. package/examples/immeuble/sources/documents/statuts-erables.md +22 -0
  182. package/examples/immeuble/sources/documents/statuts-tilleuls.md +19 -0
  183. package/examples/immeuble/sources/documents/succession-jean-dupont.md +3 -0
  184. package/examples/immeuble/sources/documents/titre-propriete-tilleuls-a3.md +1 -0
  185. package/examples/immeuble/sources/documents/trous-pedagogiques.md +3 -0
  186. package/examples/immeuble/sources/ontology/README.md +1 -0
  187. package/examples/immeuble/sources/ontology/core.yaml +444 -0
  188. package/examples/immeuble/success-criteria.yaml +35 -0
  189. package/fixtures/immeuble-demo.sqlite +0 -0
  190. package/package.json +16 -3
  191. package/scripts/lib/sqlite-runtime.mjs +1 -1
  192. package/scripts/load-immeuble-demo.sh +103 -0
  193. package/scripts/seed-immeuble-projections.mjs +75 -148
  194. package/scripts/verify-immeuble-demo-sources.mjs +93 -0
  195. package/scripts/verify-immeuble-demo.mjs +69 -0
  196. package/build/client/_app/immutable/chunks/BmeSanva.js +0 -1
  197. package/build/client/_app/immutable/chunks/BmeSanva.js.br +0 -0
  198. package/build/client/_app/immutable/chunks/BmeSanva.js.gz +0 -0
  199. package/build/client/_app/immutable/entry/app.CVz6aYsT.js.br +0 -0
  200. package/build/client/_app/immutable/entry/app.CVz6aYsT.js.gz +0 -0
  201. package/build/client/_app/immutable/entry/start.Bt5tVOz8.js +0 -1
  202. package/build/client/_app/immutable/entry/start.Bt5tVOz8.js.br +0 -2
  203. package/build/client/_app/immutable/entry/start.Bt5tVOz8.js.gz +0 -0
  204. package/build/client/_app/immutable/nodes/1.BBtxY46Q.js.br +0 -0
  205. package/build/client/_app/immutable/nodes/1.BBtxY46Q.js.gz +0 -0
  206. package/build/server/chunks/entries/endpoints/api/graph/schema-registry/_server.ts.js-Dyfsc-VA.js.map +0 -1
  207. package/scripts/build-serenity-v6-concept-review-pack.mjs +0 -493
  208. package/scripts/build-serenity-v6-review-pack.mjs +0 -479
  209. package/scripts/create-serenity-production-v6.mjs +0 -627
  210. package/scripts/export-serenity-v6-backup.mjs +0 -178
  211. package/scripts/import-serenity-v6-user-decisions.mjs +0 -543
  212. package/scripts/materialize-serenity-v6-snapshots.mjs +0 -675
@@ -0,0 +1,714 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extract and score projection candidates from ontology notes and catalog.
4
+ * Port of starterkit/scripts/analyze_projection_candidates.py for immeuble lab.
5
+ *
6
+ * Usage:
7
+ * node analyze-projection-candidates.mjs \
8
+ * --db /path/to/immeuble.sqlite \
9
+ * --workspace immeuble \
10
+ * --projection-catalog ../../contracts/projection_catalog.yaml \
11
+ * --model-contract ../../contracts/model_contract.json \
12
+ * [--include-blind-spots] [--include-jtbd] \
13
+ * [--output-dir ../../reports] [--strict]
14
+ */
15
+
16
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { parseYaml } from "../yaml-lite.mjs";
20
+ import {
21
+ LENS_PATTERNS,
22
+ VALID_ARTIFACT_KINDS,
23
+ VALID_PROJ_TYPES
24
+ } from "./analysis-lenses.mjs";
25
+ import { fmtFacetValues, knownTermsFromContract } from "./facet-prefix.mjs";
26
+ import { parseArgs, parseFlag, sqliteQuery, sqliteTableExists } from "./sqlite-utils.mjs";
27
+
28
+ /**
29
+ * @typedef {Object} AnalysisPattern
30
+ * @property {string} lens
31
+ * @property {string} name
32
+ * @property {string} label
33
+ * @property {string} business_question
34
+ * @property {string} description
35
+ * @property {string} suggested_proj_type
36
+ * @property {string[]} retrieval_jobs
37
+ * @property {string[]} kpi_hints
38
+ * @property {string[]} required_schemas
39
+ * @property {string[]} required_facets
40
+ * @property {string[]} required_edges
41
+ * @property {string[]} human_jobs
42
+ * @property {string[]} ai_agent_jobs
43
+ * @property {string} impact_summary
44
+ * @property {string[]} pattern_tags
45
+ * @property {number} [confidence]
46
+ */
47
+
48
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
49
+ const immeubleRoot = resolve(scriptDir, "..", "..");
50
+
51
+ const args = parseArgs(process.argv.slice(2));
52
+ const dbPath = resolve(parseFlag(args, "db", join(immeubleRoot, "..", "..", "..", "data", "immeuble-lab.sqlite")));
53
+ const workspaceId = parseFlag(args, "workspace", "immeuble");
54
+ const sourceDir = resolve(parseFlag(args, "source-dir", immeubleRoot));
55
+ const catalogPath = resolve(parseFlag(args, "projection-catalog", join(immeubleRoot, "contracts", "projection_catalog.yaml")));
56
+ const managerQuestionsPath = parseFlag(args, "manager-questions", "");
57
+ const modelContractPath = resolve(parseFlag(args, "model-contract", join(immeubleRoot, "contracts", "model_contract.json")));
58
+ const outputDir = resolve(parseFlag(args, "output-dir", join(immeubleRoot, "reports")));
59
+ const role = parseFlag(args, "role", "gestionnaire_syndic");
60
+ const strict = args.strict === "true";
61
+ const includeBlindSpots = args["include-blind-spots"] === "true";
62
+ const includeJtbd = args["include-jtbd"] === "true";
63
+ const recursiveMarkdown = args["recursive-markdown"] === "true";
64
+
65
+ function slugify(value) {
66
+ return String(value ?? "")
67
+ .toLowerCase()
68
+ .normalize("NFD")
69
+ .replace(/[\u0300-\u036f]/g, "")
70
+ .replace(/[^a-z0-9]+/g, "_")
71
+ .replace(/^_+|_+$/g, "")
72
+ .replace(/_+/g, "_");
73
+ }
74
+
75
+ function asList(value) {
76
+ if (value == null) return [];
77
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
78
+ if (typeof value === "string" && value) return [value];
79
+ return [String(value)];
80
+ }
81
+
82
+ function materializedLookupSqlite(dbPath, workspaceId) {
83
+ const scopes = new Set();
84
+ const liveIds = new Set();
85
+ const liveSlugs = new Set();
86
+ const legacyRefs = new Set();
87
+
88
+ if (!existsSync(dbPath)) {
89
+ return { analysis_plan_scopes: scopes, live_answer_view_ids: liveIds, live_answer_view_slugs: liveSlugs, live_answer_legacy_refs: legacyRefs };
90
+ }
91
+
92
+ if (sqliteTableExists(dbPath, "projections")) {
93
+ const ws = workspaceId.replace(/'/g, "''");
94
+ for (const row of sqliteQuery(
95
+ dbPath,
96
+ `SELECT scope FROM projections WHERE scope = '${ws}' OR scope LIKE '${ws}:%'`
97
+ )) {
98
+ if (row.scope) scopes.add(String(row.scope));
99
+ }
100
+ }
101
+
102
+ if (sqliteTableExists(dbPath, "mindbrain_answer_artifacts")) {
103
+ const ws = workspaceId.replace(/'/g, "''");
104
+ for (const row of sqliteQuery(
105
+ dbPath,
106
+ `SELECT artifact_id, slug, legacy_ref FROM mindbrain_answer_artifacts
107
+ WHERE artifact_kind = 'live_answer_view' AND (workspace_id = '${ws}' OR workspace_id IS NULL)`
108
+ )) {
109
+ if (row.artifact_id) liveIds.add(String(row.artifact_id));
110
+ if (row.slug) liveSlugs.add(String(row.slug));
111
+ if (row.legacy_ref) legacyRefs.add(String(row.legacy_ref));
112
+ }
113
+ }
114
+
115
+ return {
116
+ analysis_plan_scopes: scopes,
117
+ live_answer_view_ids: liveIds,
118
+ live_answer_view_slugs: liveSlugs,
119
+ live_answer_legacy_refs: legacyRefs
120
+ };
121
+ }
122
+
123
+ function suggestedType(jobs) {
124
+ if (jobs.includes("monitor")) return "STEP";
125
+ if (jobs.includes("aggregate") || jobs.includes("summary")) return "FACT";
126
+ return "STEP";
127
+ }
128
+
129
+ function normalizeProjType(value, jobs = []) {
130
+ const normalized = String(value ?? "").trim().toUpperCase();
131
+ if (VALID_PROJ_TYPES.has(normalized)) return { type: normalized, warning: "" };
132
+ if (normalized === "NOTE") {
133
+ const fallback = suggestedType(jobs);
134
+ return { type: fallback, warning: "NOTE is pack-ranking only; use STEP/FACT/CONSTRAINT/GOAL for ghostcrab_project" };
135
+ }
136
+ if (jobs.length) {
137
+ const fallback = suggestedType(jobs);
138
+ return { type: fallback, warning: `Unknown proj_type \`${value}\`; inferred ${fallback}` };
139
+ }
140
+ return { type: "STEP", warning: `Unknown proj_type \`${value}\`; defaulting to STEP` };
141
+ }
142
+
143
+ function inferArtifactKind(jobs, label, description, explicit) {
144
+ if (explicit && VALID_ARTIFACT_KINDS.has(explicit)) return explicit;
145
+ const text = `${label} ${description}`.toLowerCase();
146
+ if (
147
+ jobs.includes("monitor") &&
148
+ ["tableau", "dashboard", "direct", "temps reel", "live", "quotidien", "journalier"].some((w) => text.includes(w))
149
+ ) {
150
+ return "live_answer_view";
151
+ }
152
+ return "analysis_plan";
153
+ }
154
+
155
+ function inferMaterializationTarget(artifactKind, origin) {
156
+ if (origin === "manager_questions") return "review_only";
157
+ if (artifactKind === "live_answer_view") return "answer_artifact_seed";
158
+ if (artifactKind === "analysis_plan") return "ghostcrab_project";
159
+ return "review_only";
160
+ }
161
+
162
+ function materializationStatusForCandidate(candidate, lookup) {
163
+ if (candidate.suggested_artifact_kind === "live_answer_view") {
164
+ const slug = candidate.name;
165
+ const artifactId = `live_answer_view__${slug}`;
166
+ if (
167
+ lookup.live_answer_view_ids.has(artifactId) ||
168
+ lookup.live_answer_view_slugs.has(slug) ||
169
+ lookup.live_answer_legacy_refs.has(candidate.expected_scope)
170
+ ) {
171
+ return "materialized";
172
+ }
173
+ return "candidate";
174
+ }
175
+ if (lookup.analysis_plan_scopes.has(candidate.expected_scope)) return "materialized";
176
+ return "candidate";
177
+ }
178
+
179
+ function finalizeCandidateFields(input, lookup) {
180
+ const jobs = input.retrieval_jobs || ["summary"];
181
+ const { type: suggestedProjType, warning } = normalizeProjType(input.proj_type, jobs);
182
+ const suggestedArtifactKind = inferArtifactKind(jobs, input.label, input.description, input.artifact_kind);
183
+ const materializationTarget = inferMaterializationTarget(suggestedArtifactKind, input.origin || "source_table");
184
+
185
+ /** @type {Record<string, unknown>} */
186
+ const candidate = {
187
+ name: input.name,
188
+ label: input.label,
189
+ ontology: input.ontology,
190
+ description: input.description,
191
+ source_file: input.source_file,
192
+ source_section: input.source_section,
193
+ expected_scope: input.expected_scope,
194
+ suggested_proj_type: suggestedProjType,
195
+ suggested_artifact_kind: suggestedArtifactKind,
196
+ materialization_target: materializationTarget,
197
+ retrieval_jobs: jobs,
198
+ kpi_hints: input.kpi_hints || [],
199
+ data_dependencies: input.data_dependencies || input.required_schemas || [],
200
+ materialization_status: "candidate",
201
+ recommendation: input.recommendation || "review",
202
+ business_question: input.business_question || "",
203
+ origin: input.origin || "source_table",
204
+ lens: input.lens || "",
205
+ role: input.role || "",
206
+ materialization_warning: warning,
207
+ required_schemas: input.required_schemas || [],
208
+ required_facets: input.required_facets || [],
209
+ required_edges: input.required_edges || [],
210
+ human_jobs: input.human_jobs || [],
211
+ ai_agent_jobs: input.ai_agent_jobs || [],
212
+ impact_summary: input.impact_summary || "",
213
+ pattern_tags: input.pattern_tags || [],
214
+ confidence: input.confidence ?? 1.0
215
+ };
216
+
217
+ candidate.materialization_status = materializationStatusForCandidate(candidate, lookup);
218
+ if (!input.recommendation) {
219
+ if (candidate.materialization_status === "materialized") candidate.recommendation = "keep";
220
+ else if (materializationTarget === "review_only") candidate.recommendation = "review";
221
+ else if (candidate.data_dependencies.length || candidate.required_schemas.length) candidate.recommendation = "add";
222
+ else candidate.recommendation = "review";
223
+ }
224
+ return candidate;
225
+ }
226
+
227
+ function extractProjectionSection(markdown) {
228
+ const match = markdown.match(/^## Projections \/ rapports types\s*$/m);
229
+ if (!match) return "";
230
+ const start = match.index + match[0].length;
231
+ const rest = markdown.slice(start);
232
+ const next = rest.match(/^##\s+/m);
233
+ return next ? rest.slice(0, next.index) : rest;
234
+ }
235
+
236
+ function parseMarkdownTable(section) {
237
+ const rows = [];
238
+ for (const line of section.split("\n")) {
239
+ const trimmed = line.trim();
240
+ if (!trimmed.startsWith("|") || trimmed.includes("---")) continue;
241
+ const cells = trimmed.slice(1, -1).split("|").map((c) => c.trim());
242
+ if (cells.length < 2 || ["projection", "rapport"].includes(cells[0].toLowerCase())) continue;
243
+ rows.push([cells[0], cells[1]]);
244
+ }
245
+ return rows;
246
+ }
247
+
248
+ function inferRetrievalJobs(label, description) {
249
+ const text = `${label} ${description}`.toLowerCase();
250
+ const jobs = [];
251
+ if (["liste", "annuaire", "calendrier", "historique"].some((w) => text.includes(w))) jobs.push("list");
252
+ if (["suivi", "en cours", "retard", "alerte", "echeance"].some((w) => text.includes(w))) jobs.push("monitor");
253
+ if (["fiche", "vue complete", "situation"].some((w) => text.includes(w))) jobs.push("summary");
254
+ if (["repartition", "comparaison", "par "].some((w) => text.includes(w))) jobs.push("aggregate");
255
+ if (["chaine", "roles", "multi", "impact"].some((w) => text.includes(w))) jobs.push("graph_traversal");
256
+ return jobs.length ? jobs : ["summary"];
257
+ }
258
+
259
+ function extractMarkdownCandidates(sourceDir, dbPath, workspaceId, recursive) {
260
+ const lookup = materializedLookupSqlite(dbPath, workspaceId);
261
+ const candidates = [];
262
+ const paths = [];
263
+
264
+ function walk(dir) {
265
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
266
+ const full = join(dir, entry.name);
267
+ if (entry.isDirectory() && recursive) walk(full);
268
+ else if (entry.isFile() && entry.name.endsWith(".md")) paths.push(full);
269
+ }
270
+ }
271
+
272
+ if (recursive) walk(sourceDir);
273
+ else {
274
+ try {
275
+ for (const name of readdirSync(sourceDir)) {
276
+ if (name.endsWith(".md")) paths.push(join(sourceDir, name));
277
+ }
278
+ } catch {
279
+ // source dir may not exist
280
+ }
281
+ }
282
+
283
+ for (const path of paths.sort()) {
284
+ const markdown = readFileSync(path, "utf8");
285
+ const section = extractProjectionSection(markdown);
286
+ if (!section) continue;
287
+ const ontology = slugify(path.split("/").pop()?.replace(/\.md$/, "") ?? "catalog");
288
+ for (const [label, description] of parseMarkdownTable(section)) {
289
+ const name = slugify(label);
290
+ candidates.push(
291
+ finalizeCandidateFields(
292
+ {
293
+ name,
294
+ label,
295
+ ontology,
296
+ description,
297
+ source_file: path,
298
+ source_section: "Projections / rapports types",
299
+ expected_scope: `${workspaceId}:${ontology}:${name}`,
300
+ retrieval_jobs: inferRetrievalJobs(label, description),
301
+ kpi_hints: [],
302
+ data_dependencies: [],
303
+ business_question: description.endsWith("?") ? description : "",
304
+ required_schemas: [],
305
+ origin: "source_table"
306
+ },
307
+ lookup
308
+ )
309
+ );
310
+ }
311
+ }
312
+ return candidates;
313
+ }
314
+
315
+ function extractProjectionCatalogCandidates(catalogPath, dbPath, workspaceId) {
316
+ if (!existsSync(catalogPath)) return [];
317
+ const payload = parseYaml(readFileSync(catalogPath, "utf8"));
318
+ const lookup = materializedLookupSqlite(dbPath, workspaceId);
319
+ const candidates = [];
320
+
321
+ for (const item of payload.projections ?? []) {
322
+ if (!item || typeof item !== "object") continue;
323
+ const name = slugify(item.name || item.label || item.business_question || "projection");
324
+ const scope = String(item.scope || `${workspaceId}:catalog:${name}`);
325
+ const parts = scope.split(":");
326
+ const ontology = parts.length > 2 && parts[0] === workspaceId ? parts[1] : "catalog";
327
+ const jobs = asList(item.retrieval_jobs).length ? asList(item.retrieval_jobs) : ["summary"];
328
+ const requiredSchemas = asList(item.required_schemas).map((s) =>
329
+ s.includes(":") ? s : `immeuble:core:${s}`
330
+ );
331
+
332
+ candidates.push(
333
+ finalizeCandidateFields(
334
+ {
335
+ name,
336
+ label: String(item.label || name),
337
+ ontology,
338
+ description: String(item.description || item.business_question || ""),
339
+ source_file: catalogPath,
340
+ source_section: "projection_catalog.yaml",
341
+ expected_scope: scope,
342
+ retrieval_jobs: jobs,
343
+ kpi_hints: asList(item.kpi_hints),
344
+ data_dependencies: requiredSchemas,
345
+ recommendation: lookup.analysis_plan_scopes.has(scope) ? "keep" : "add",
346
+ business_question: String(item.business_question || ""),
347
+ origin: "projection_catalog",
348
+ required_schemas: requiredSchemas,
349
+ required_facets: asList(item.required_facets),
350
+ required_edges: asList(item.required_edges),
351
+ impact_summary: "Projection declaree dans le catalogue decisionnel.",
352
+ confidence: 1.0,
353
+ proj_type: String(item.proj_type || ""),
354
+ artifact_kind: String(item.artifact_kind || "") || undefined
355
+ },
356
+ lookup
357
+ )
358
+ );
359
+ }
360
+ return candidates;
361
+ }
362
+
363
+ function extractManagerQuestionCandidates(path, dbPath, workspaceId) {
364
+ if (!path || !existsSync(path)) return [];
365
+ const payload = parseYaml(readFileSync(path, "utf8"));
366
+ const lookup = materializedLookupSqlite(dbPath, workspaceId);
367
+ const scopes = lookup.analysis_plan_scopes;
368
+ const candidates = [];
369
+
370
+ for (const [family, questions] of Object.entries(payload.families ?? {})) {
371
+ if (!Array.isArray(questions)) continue;
372
+ for (const question of questions) {
373
+ if (!question || typeof question !== "object") continue;
374
+ const projection = String(question.projection || "");
375
+ const qText = String(question.question || "");
376
+ if (!qText) continue;
377
+ const name = slugify(projection || question.id || qText);
378
+ let scope = `${workspaceId}:${slugify(String(family))}:${name}`;
379
+ const matching = [...scopes].find((s) => projection && s.endsWith(`:${projection}`));
380
+ if (matching) scope = matching;
381
+
382
+ candidates.push(
383
+ finalizeCandidateFields(
384
+ {
385
+ name,
386
+ label: qText,
387
+ ontology: slugify(String(family)),
388
+ description: qText,
389
+ source_file: path,
390
+ source_section: "manager_questions.yaml",
391
+ expected_scope: scope,
392
+ retrieval_jobs: ["summary"],
393
+ recommendation: scopes.has(scope) ? "keep" : "review",
394
+ business_question: qText,
395
+ origin: "manager_questions",
396
+ impact_summary: `Question manager associee a la projection \`${projection}\`.`,
397
+ proj_type: "NOTE"
398
+ },
399
+ lookup
400
+ )
401
+ );
402
+ }
403
+ }
404
+ return candidates;
405
+ }
406
+
407
+ function candidateSignature(candidate) {
408
+ return new Set([candidate.name, slugify(candidate.label), slugify(candidate.business_question || candidate.description)]);
409
+ }
410
+
411
+ function appendUniqueCandidates(base, additions) {
412
+ const seen = new Set();
413
+ const merged = [];
414
+ for (const candidate of [...base, ...additions]) {
415
+ const signature = `${candidate.origin}|${candidate.expected_scope}|${slugify(candidate.business_question || candidate.label)}`;
416
+ if (seen.has(signature)) continue;
417
+ seen.add(signature);
418
+ merged.push(candidate);
419
+ }
420
+ return merged;
421
+ }
422
+
423
+ function extractLensCandidates(workspaceId, dbPath, lenses, roleName, sourceCandidates) {
424
+ const lookup = materializedLookupSqlite(dbPath, workspaceId);
425
+ const existing = new Set();
426
+ for (const c of sourceCandidates) {
427
+ for (const sig of candidateSignature(c)) existing.add(sig);
428
+ }
429
+
430
+ const candidates = [];
431
+ for (const lens of lenses) {
432
+ for (const pattern of LENS_PATTERNS[lens] ?? []) {
433
+ const name = slugify(pattern.name);
434
+ if (existing.has(name) || existing.has(slugify(pattern.business_question))) continue;
435
+ candidates.push(
436
+ finalizeCandidateFields(
437
+ {
438
+ name,
439
+ label: pattern.label,
440
+ ontology: slugify(roleName),
441
+ description: pattern.description,
442
+ source_file: "analysis_lens",
443
+ source_section: pattern.lens,
444
+ expected_scope: `${workspaceId}:${slugify(roleName)}:${name}`,
445
+ retrieval_jobs: pattern.retrieval_jobs,
446
+ kpi_hints: pattern.kpi_hints,
447
+ data_dependencies: [...new Set(pattern.required_schemas)].sort(),
448
+ recommendation: "add",
449
+ business_question: pattern.business_question,
450
+ origin: "analysis_lens",
451
+ lens: pattern.lens,
452
+ role: roleName,
453
+ required_schemas: pattern.required_schemas,
454
+ required_facets: pattern.required_facets,
455
+ required_edges: pattern.required_edges,
456
+ human_jobs: pattern.human_jobs,
457
+ ai_agent_jobs: pattern.ai_agent_jobs,
458
+ impact_summary: pattern.impact_summary,
459
+ pattern_tags: pattern.pattern_tags,
460
+ confidence: pattern.confidence ?? 0.8,
461
+ proj_type: pattern.suggested_proj_type
462
+ },
463
+ lookup
464
+ )
465
+ );
466
+ }
467
+ }
468
+ return candidates;
469
+ }
470
+
471
+ function selectedLenses() {
472
+ const lenses = [];
473
+ if (includeBlindSpots) lenses.push("blind_spot_manager");
474
+ if (includeJtbd) lenses.push("jtbd_human", "jtbd_ai");
475
+ return lenses.filter((l, i, arr) => LENS_PATTERNS[l] && arr.indexOf(l) === i);
476
+ }
477
+
478
+ function collectModelImpacts(candidates) {
479
+ const impactCandidates = candidates.filter((c) => ["analysis_lens", "llm_review"].includes(c.origin));
480
+ return {
481
+ lens_candidate_count: impactCandidates.length,
482
+ by_lens: Object.fromEntries(
483
+ [...new Set(impactCandidates.map((c) => c.lens))].map((l) => [l, impactCandidates.filter((c) => c.lens === l).length])
484
+ ),
485
+ required_schemas: [...new Set(impactCandidates.flatMap((c) => c.required_schemas || []))].sort(),
486
+ required_facets: [...new Set(impactCandidates.flatMap((c) => c.required_facets || []))].sort(),
487
+ required_edges: [...new Set(impactCandidates.flatMap((c) => c.required_edges || []))].sort()
488
+ };
489
+ }
490
+
491
+ function collectValidationGaps(candidates, contract) {
492
+ const sourceOrigins = new Set(["source_table", "projection_catalog", "manager_questions"]);
493
+ const proposalOrigins = new Set(["analysis_lens", "llm_review"]);
494
+ const sourceCandidates = candidates.filter((c) => sourceOrigins.has(c.origin));
495
+ const proposalCandidates = candidates.filter((c) => proposalOrigins.has(c.origin));
496
+
497
+ const sourceSchemas = new Set(sourceCandidates.flatMap((c) => c.required_schemas || []));
498
+ const sourceFacets = new Set(sourceCandidates.flatMap((c) => c.required_facets || []));
499
+ const sourceEdges = new Set(sourceCandidates.flatMap((c) => c.required_edges || []));
500
+ const proposalSchemas = new Set(proposalCandidates.flatMap((c) => c.required_schemas || []));
501
+ const proposalFacets = new Set(proposalCandidates.flatMap((c) => c.required_facets || []));
502
+ const proposalEdges = new Set(proposalCandidates.flatMap((c) => c.required_edges || []));
503
+
504
+ const terms = knownTermsFromContract(contract);
505
+ return {
506
+ extension_schemas: [...proposalSchemas].filter((s) => !sourceSchemas.has(s)).sort(),
507
+ extension_facets: [...proposalFacets].filter((f) => !sourceFacets.has(f)).sort(),
508
+ extension_edges: [...proposalEdges].filter((e) => !sourceEdges.has(e)).sort(),
509
+ unknown_schemas: contract && Object.keys(contract).length ? [...proposalSchemas].filter((s) => !terms.schemas.has(s)).sort() : [],
510
+ unknown_facets: contract && Object.keys(contract).length ? [...proposalFacets].filter((f) => !terms.facets.has(f)).sort() : [],
511
+ unknown_edges: contract && Object.keys(contract).length ? [...proposalEdges].filter((e) => !terms.edges.has(e)).sort() : [],
512
+ contract_checked: Boolean(contract && Object.keys(contract).length)
513
+ };
514
+ }
515
+
516
+ function writeValidationMarkdown(payload, path) {
517
+ const byOrigin = {};
518
+ for (const item of payload.candidates) {
519
+ const origin = item.origin || "source_table";
520
+ if (!byOrigin[origin]) byOrigin[origin] = [];
521
+ byOrigin[origin].push(item);
522
+ }
523
+ const gaps = payload.validation_gaps || {};
524
+ const lines = [
525
+ "# Projection Model Validation",
526
+ "",
527
+ "## Synthese",
528
+ "",
529
+ `- Workspace: \`${payload.workspace_id}\``,
530
+ `- Projections catalogue: ${payload.summary.projection_catalog_count}`,
531
+ `- Questions manager: ${payload.summary.manager_questions_count}`,
532
+ `- Ajouts par patterns: ${payload.summary.analysis_lens_count}`,
533
+ `- Scopes materialises: ${payload.summary.unique_materialized_scope_count}`,
534
+ "",
535
+ "## Projections catalogue",
536
+ ""
537
+ ];
538
+
539
+ for (const item of byOrigin.projection_catalog ?? []) {
540
+ lines.push(
541
+ `### ${item.label}`,
542
+ `- Question: ${item.business_question || item.description}`,
543
+ `- Scope: \`${item.expected_scope}\``,
544
+ `- artifact_kind: \`${item.suggested_artifact_kind}\``,
545
+ `- Statut: \`${item.materialization_status}\``,
546
+ `- Facettes requises: ${fmtFacetValues(item.required_facets || [])}`,
547
+ `- Aretes requises: ${fmtFacetValues(item.required_edges || [])}`,
548
+ ""
549
+ );
550
+ }
551
+
552
+ const proposed = [...(byOrigin.analysis_lens ?? []), ...(byOrigin.llm_review ?? [])];
553
+ if (proposed.length) {
554
+ lines.push("## Questions manquantes proposees", "");
555
+ for (const item of proposed) {
556
+ lines.push(
557
+ `### ${item.label}`,
558
+ `- Categorie: \`${item.lens || item.origin}\``,
559
+ `- Question: ${item.business_question || item.description}`,
560
+ `- Facettes requises: ${fmtFacetValues(item.required_facets || [])}`,
561
+ `- Aretes requises: ${fmtFacetValues(item.required_edges || [])}`,
562
+ ""
563
+ );
564
+ }
565
+ }
566
+
567
+ lines.push(
568
+ "## Dimensions et graphes a valider",
569
+ "",
570
+ "### Facettes nouvelles par rapport aux sources",
571
+ "",
572
+ fmtFacetValues(gaps.extension_facets || []),
573
+ "",
574
+ "### Aretes nouvelles par rapport aux sources",
575
+ "",
576
+ fmtFacetValues(gaps.extension_edges || []),
577
+ ""
578
+ );
579
+
580
+ if (gaps.contract_checked) {
581
+ lines.push(
582
+ "## Gaps versus model_contract",
583
+ "",
584
+ `- Schemas inconnus: ${fmtFacetValues(gaps.unknown_schemas || [])}`,
585
+ `- Facettes inconnues: ${fmtFacetValues(gaps.unknown_facets || [])}`,
586
+ `- Aretes inconnues: ${fmtFacetValues(gaps.unknown_edges || [])}`,
587
+ ""
588
+ );
589
+ }
590
+
591
+ writeFileSync(path, lines.join("\n") + "\n", "utf8");
592
+ }
593
+
594
+ function writeCandidatesMarkdown(payload, path) {
595
+ const lines = [
596
+ "# Projection Candidate Review",
597
+ "",
598
+ `- Workspace: \`${payload.workspace_id}\``,
599
+ `- Generated at: \`${payload.generated_at}\``,
600
+ `- Candidate count: ${payload.summary.total_count}`,
601
+ `- Materialized count: ${payload.summary.materialized_count}`,
602
+ `- Active lenses: ${payload.active_lenses.join(", ") || "n/a"}`,
603
+ ""
604
+ ];
605
+
606
+ for (const [ontology, items] of Object.entries(payload.by_ontology)) {
607
+ lines.push(`## ${ontology}`, "");
608
+ for (const item of items) {
609
+ lines.push(
610
+ `### ${item.label}`,
611
+ `- Scope: \`${item.expected_scope}\``,
612
+ `- Status: \`${item.materialization_status}\``,
613
+ `- Origin: \`${item.origin}\``,
614
+ `- artifact_kind: \`${item.suggested_artifact_kind}\``,
615
+ `- Required facets: ${fmtFacetValues(item.required_facets || [])}`,
616
+ `- Required edges: ${fmtFacetValues(item.required_edges || [])}`,
617
+ ""
618
+ );
619
+ }
620
+ }
621
+ writeFileSync(path, lines.join("\n") + "\n", "utf8");
622
+ }
623
+
624
+ function strictPlanFailed(gaps) {
625
+ if (!gaps.contract_checked) return false;
626
+ return (
627
+ (gaps.unknown_schemas?.length ?? 0) > 0 ||
628
+ (gaps.unknown_facets?.length ?? 0) > 0 ||
629
+ (gaps.unknown_edges?.length ?? 0) > 0
630
+ );
631
+ }
632
+
633
+ function main() {
634
+ const lenses = selectedLenses();
635
+ let candidates = appendUniqueCandidates(
636
+ [],
637
+ extractMarkdownCandidates(sourceDir, dbPath, workspaceId, recursiveMarkdown)
638
+ );
639
+ candidates = appendUniqueCandidates(candidates, extractProjectionCatalogCandidates(catalogPath, dbPath, workspaceId));
640
+ if (managerQuestionsPath) {
641
+ candidates = appendUniqueCandidates(
642
+ candidates,
643
+ extractManagerQuestionCandidates(resolve(managerQuestionsPath), dbPath, workspaceId)
644
+ );
645
+ }
646
+ candidates = appendUniqueCandidates(
647
+ candidates,
648
+ extractLensCandidates(workspaceId, dbPath, lenses, role, candidates)
649
+ );
650
+
651
+ let modelContract = {};
652
+ try {
653
+ modelContract = JSON.parse(readFileSync(modelContractPath, "utf8"));
654
+ } catch {
655
+ modelContract = {};
656
+ }
657
+
658
+ const byOntology = {};
659
+ const byArtifactKind = {};
660
+ for (const candidate of candidates) {
661
+ if (!byOntology[candidate.ontology]) byOntology[candidate.ontology] = [];
662
+ byOntology[candidate.ontology].push(candidate);
663
+ const kind = candidate.suggested_artifact_kind || "analysis_plan";
664
+ byArtifactKind[kind] = (byArtifactKind[kind] || 0) + 1;
665
+ }
666
+
667
+ const validationGaps = collectValidationGaps(candidates, modelContract);
668
+ const payload = {
669
+ workspace_id: workspaceId,
670
+ db_path: dbPath,
671
+ generated_at: new Date().toISOString(),
672
+ active_lenses: lenses,
673
+ role,
674
+ model_contract_path: modelContractPath,
675
+ summary: {
676
+ candidate_count: candidates.filter((c) => c.materialization_status === "candidate").length,
677
+ materialized_count: candidates.filter((c) => c.materialization_status === "materialized").length,
678
+ unique_materialized_scope_count: new Set(
679
+ candidates.filter((c) => c.materialization_status === "materialized").map((c) => c.expected_scope)
680
+ ).size,
681
+ analysis_lens_count: candidates.filter((c) => c.origin === "analysis_lens").length,
682
+ projection_catalog_count: candidates.filter((c) => c.origin === "projection_catalog").length,
683
+ manager_questions_count: candidates.filter((c) => c.origin === "manager_questions").length,
684
+ source_table_count: candidates.filter((c) => c.origin === "source_table").length,
685
+ total_count: candidates.length,
686
+ by_artifact_kind: Object.fromEntries(Object.entries(byArtifactKind).sort())
687
+ },
688
+ model_impacts: collectModelImpacts(candidates),
689
+ validation_gaps: validationGaps,
690
+ by_ontology: Object.fromEntries(Object.entries(byOntology).sort()),
691
+ candidates
692
+ };
693
+
694
+ mkdirSync(outputDir, { recursive: true });
695
+ const jsonPath = join(outputDir, "projection_candidates.json");
696
+ const mdPath = join(outputDir, "projection_candidates.md");
697
+ const validationPath = join(outputDir, "projection_model_validation.md");
698
+ writeFileSync(jsonPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
699
+ writeCandidatesMarkdown(payload, mdPath);
700
+ writeValidationMarkdown(payload, validationPath);
701
+
702
+ const output = {
703
+ json: jsonPath,
704
+ markdown: mdPath,
705
+ validation_markdown: validationPath,
706
+ summary: payload.summary,
707
+ validation_gaps: validationGaps,
708
+ ok: !strict || !strictPlanFailed(validationGaps)
709
+ };
710
+ console.log(JSON.stringify(output, null, 2));
711
+ if (strict && strictPlanFailed(validationGaps)) process.exit(1);
712
+ }
713
+
714
+ main();