@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.
- package/README.md +16 -8
- package/build/client/_app/immutable/chunks/{D0UIlUGZ.js → CIErFlYG.js} +1 -1
- package/build/client/_app/immutable/chunks/CIErFlYG.js.br +0 -0
- package/build/client/_app/immutable/chunks/CIErFlYG.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CR-imLox.js → app.mURYm8o_.js} +2 -2
- package/build/client/_app/immutable/entry/app.mURYm8o_.js.br +0 -0
- package/build/client/_app/immutable/entry/app.mURYm8o_.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.D4M9ZeGO.js +1 -0
- package/build/client/_app/immutable/entry/start.D4M9ZeGO.js.br +2 -0
- package/build/client/_app/immutable/entry/start.D4M9ZeGO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.CTEedoSY.js → 1.DHKtMeFI.js} +1 -1
- package/build/client/_app/immutable/nodes/1.DHKtMeFI.js.br +1 -0
- package/build/client/_app/immutable/nodes/1.DHKtMeFI.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +4 -4
- package/build/index.js +4 -4
- package/build/server/chunks/chunks/{internal.js-WOmQXGMa.js → internal.js-C3tV0XXj.js} +2 -2
- package/build/server/chunks/chunks/{internal.js-WOmQXGMa.js.map → internal.js-C3tV0XXj.js.map} +1 -1
- package/build/server/chunks/{handler-BIDedSZq.js → handler-DlaCCnxx.js} +3 -3
- package/build/server/chunks/{handler-BIDedSZq.js.map → handler-DlaCCnxx.js.map} +1 -1
- package/build/server/chunks/{index.js-YVPJa0so.js → index.js-BGfKWHak.js} +2 -2
- package/build/server/chunks/{index.js-YVPJa0so.js.map → index.js-BGfKWHak.js.map} +1 -1
- package/build/server/chunks/{manifest.js-aGRKuiqF.js → manifest.js-CnmaNf5D.js} +3 -3
- package/build/server/chunks/{manifest.js-aGRKuiqF.js.map → manifest.js-CnmaNf5D.js.map} +1 -1
- package/build/server/chunks/nodes/{1.js-Dhh3ErZY.js → 1.js-BypjwBYB.js} +2 -2
- package/build/server/chunks/nodes/{1.js-Dhh3ErZY.js.map → 1.js-BypjwBYB.js.map} +1 -1
- package/examples/immeuble/ACCEPTANCE.yaml +82 -0
- package/examples/immeuble/BIM_LITE.md +15 -0
- package/examples/immeuble/CHECKLIST.md +128 -0
- package/examples/immeuble/README.md +121 -0
- package/examples/immeuble/bundle/immeuble.bundle.json +22786 -0
- package/examples/immeuble/contracts/answer_artifacts.seed.jsonl +34 -0
- package/examples/immeuble/contracts/business_capabilities.seed.jsonl +25 -0
- package/examples/immeuble/contracts/consumer_contract.yaml +131 -0
- package/examples/immeuble/contracts/immeuble_structured_import_model.json +310 -0
- package/examples/immeuble/contracts/mapping_external_to_canonical.json +375 -0
- package/examples/immeuble/contracts/mapping_external_to_canonical.yaml +55 -0
- package/examples/immeuble/contracts/mapping_external_to_canonical_ws.json +65 -0
- package/examples/immeuble/contracts/model_contract.json +943 -0
- package/examples/immeuble/contracts/projection_catalog.yaml +366 -0
- package/examples/immeuble/contracts/scenarios.yaml +27 -0
- package/examples/immeuble/contracts/semantic_proposal.golden.json +1 -0
- package/examples/immeuble/contracts/source_profile.yaml +38 -0
- package/examples/immeuble/fake_data/DeltaFinding.csv +20 -0
- package/examples/immeuble/fake_data/ProjectionResult.csv +13 -0
- package/examples/immeuble/fake_data/ag_meeting.csv +4 -0
- package/examples/immeuble/fake_data/agenda_item.csv +4 -0
- package/examples/immeuble/fake_data/architect.csv +3 -0
- package/examples/immeuble/fake_data/architecture_firm.csv +2 -0
- package/examples/immeuble/fake_data/bank_account.csv +3 -0
- package/examples/immeuble/fake_data/billing_group.csv +41 -0
- package/examples/immeuble/fake_data/block.csv +10 -0
- package/examples/immeuble/fake_data/budget_line.csv +3 -0
- package/examples/immeuble/fake_data/building.csv +6 -0
- package/examples/immeuble/fake_data/cellar.csv +41 -0
- package/examples/immeuble/fake_data/change_order.csv +2 -0
- package/examples/immeuble/fake_data/charge_call.csv +58 -0
- package/examples/immeuble/fake_data/claim.csv +4 -0
- package/examples/immeuble/fake_data/coda_entry.csv +49 -0
- package/examples/immeuble/fake_data/compliance_certificate.csv +10 -0
- package/examples/immeuble/fake_data/contractor.csv +4 -0
- package/examples/immeuble/fake_data/decision.csv +5 -0
- package/examples/immeuble/fake_data/defect_reserve.csv +3 -0
- package/examples/immeuble/fake_data/household.csv +41 -0
- package/examples/immeuble/fake_data/inspection.csv +3 -0
- package/examples/immeuble/fake_data/insurance_policy.csv +4 -0
- package/examples/immeuble/fake_data/intervention.csv +13 -0
- package/examples/immeuble/fake_data/invoice.csv +3 -0
- package/examples/immeuble/fake_data/lease_contract.csv +12 -0
- package/examples/immeuble/fake_data/maintenance_ticket.csv +13 -0
- package/examples/immeuble/fake_data/meter.csv +4 -0
- package/examples/immeuble/fake_data/meter_reading.csv +19 -0
- package/examples/immeuble/fake_data/milestone.csv +3 -0
- package/examples/immeuble/fake_data/organization.csv +12 -0
- package/examples/immeuble/fake_data/parking_space.csv +8 -0
- package/examples/immeuble/fake_data/permit.csv +3 -0
- package/examples/immeuble/fake_data/person.csv +85 -0
- package/examples/immeuble/fake_data/private_garden.csv +7 -0
- package/examples/immeuble/fake_data/progress_event.csv +2 -0
- package/examples/immeuble/fake_data/quote.csv +3 -0
- package/examples/immeuble/fake_data/receipt.csv +2 -0
- package/examples/immeuble/fake_data/reminder.csv +11 -0
- package/examples/immeuble/fake_data/service_contract.csv +4 -0
- package/examples/immeuble/fake_data/shared_equipment.csv +8 -0
- package/examples/immeuble/fake_data/shared_space.csv +10 -0
- package/examples/immeuble/fake_data/unit.csv +41 -0
- package/examples/immeuble/fake_data/work_package.csv +4 -0
- package/examples/immeuble/fake_data/worksite_project.csv +3 -0
- package/examples/immeuble/gap-rules/L0-patrimoine.json +57 -0
- package/examples/immeuble/gap-rules/L1-syndic-naive.json +36 -0
- package/examples/immeuble/gap-rules/L1-syndic.json +61 -0
- package/examples/immeuble/gap-rules/L2-chantier.json +87 -0
- package/examples/immeuble/gap-rules/L2-exploitation.json +87 -0
- package/examples/immeuble/gap-rules/L2-finance.json +48 -0
- package/examples/immeuble/gap-rules/L2-maintenance.json +107 -0
- package/examples/immeuble/gap-rules/L2-syndic-filtered.json +39 -0
- package/examples/immeuble/gap-rules/L3-full.json +332 -0
- package/examples/immeuble/gap-rules/closed-world-contract.md +76 -0
- package/examples/immeuble/gap-rules/demo.json +51 -0
- package/examples/immeuble/gap-rules/expected-findings.yaml +100 -0
- package/examples/immeuble/gap-rules/gap-scenarios.yaml +79 -0
- package/examples/immeuble/gap-rules/motifs.json +38 -0
- package/examples/immeuble/gap-rules/syndic.json +40 -0
- package/examples/immeuble/import_manifest.yaml +25 -0
- package/examples/immeuble/import_ready/graph_edges_import.csv +848 -0
- package/examples/immeuble/import_ready/mfo_facets_import.csv +559 -0
- package/examples/immeuble/index.md +140 -0
- package/examples/immeuble/model/immeuble_model.json +418 -0
- package/examples/immeuble/reports/01-model.validation.json +56 -0
- package/examples/immeuble/reports/02-mapping.validation.json +9 -0
- package/examples/immeuble/reports/acceptance.validation.json +161 -0
- package/examples/immeuble/reports/consumer_contract.validation.json +162 -0
- package/examples/immeuble/reports/graph_edges.jsonl +847 -0
- package/examples/immeuble/reports/graph_nodes.jsonl +558 -0
- package/examples/immeuble/reports/hybrid-compare.json +144 -0
- package/examples/immeuble/reports/immeuble-import-scenario.json +233 -0
- package/examples/immeuble/reports/live-artifacts-refresh.validation.json +51 -0
- package/examples/immeuble/reports/pipeline_audit.json +59 -0
- package/examples/immeuble/reports/projection_audit.json +69 -0
- package/examples/immeuble/reports/projection_audit_immeuble.json +257 -0
- package/examples/immeuble/reports/projection_audit_immeuble.md +122 -0
- package/examples/immeuble/reports/projection_candidates.json +1596 -0
- package/examples/immeuble/reports/projection_candidates.md +117 -0
- package/examples/immeuble/reports/projection_model_validation.md +115 -0
- package/examples/immeuble/reports/reindex.json +4 -0
- package/examples/immeuble/reports/reset-immeuble-workspace.json +32 -0
- package/examples/immeuble/reports/schema-id-prefix-check.json +5 -0
- package/examples/immeuble/scripts/audit-immeuble-projections.mjs +254 -0
- package/examples/immeuble/scripts/build-immeuble-model.mjs +308 -0
- package/examples/immeuble/scripts/compare-immeuble-snapshots.sh +227 -0
- package/examples/immeuble/scripts/enrich-immeuble-demo.mjs +1135 -0
- package/examples/immeuble/scripts/reset-immeuble-workspace.mjs +139 -0
- package/examples/immeuble/scripts/run-immeuble-backend.sh +164 -0
- package/examples/immeuble/scripts/run-immeuble-import.mjs +232 -0
- package/examples/immeuble/scripts/run-immeuble-live-lab.sh +439 -0
- package/examples/immeuble/scripts/seed-immeuble-gap-rules.mjs +69 -0
- package/examples/immeuble/scripts/start-immeuble-demo.sh +52 -0
- package/examples/immeuble/scripts/starterkit/analysis-lenses.mjs +181 -0
- package/examples/immeuble/scripts/starterkit/analyze-projection-candidates.mjs +714 -0
- package/examples/immeuble/scripts/starterkit/audit-ghostcrab-projections.mjs +674 -0
- package/examples/immeuble/scripts/starterkit/facet-prefix.mjs +166 -0
- package/examples/immeuble/scripts/starterkit/sqlite-utils.mjs +131 -0
- package/examples/immeuble/scripts/verify-immeuble-acceptance.mjs +284 -0
- package/examples/immeuble/scripts/verify-immeuble-live-artifacts.mjs +140 -0
- package/examples/immeuble/scripts/yaml-lite.mjs +96 -0
- package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites-immo-mcp.md +161 -0
- package/examples/immeuble/sources/agent-prompts/prompts/00-prerequisites.md +39 -0
- package/examples/immeuble/sources/agent-prompts/prompts/01-discovery-and-model-proposal.md +42 -0
- package/examples/immeuble/sources/agent-prompts/prompts/02-ontology-register.md +44 -0
- package/examples/immeuble/sources/agent-prompts/prompts/03-gap-rules-design.md +44 -0
- package/examples/immeuble/sources/agent-prompts/prompts/04-document-ingest.md +40 -0
- package/examples/immeuble/sources/agent-prompts/prompts/05-graph-extraction.md +44 -0
- package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-immo-mcp.md +109 -0
- package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare-test-immo-mcp3.md +30 -0
- package/examples/immeuble/sources/agent-prompts/prompts/06-validate-and-compare.md +63 -0
- package/examples/immeuble/sources/checklists/gap-rules-checklist.md +42 -0
- package/examples/immeuble/sources/checklists/ontology-checklist.md +54 -0
- package/examples/immeuble/sources/documents/README.md +42 -0
- package/examples/immeuble/sources/documents/annexes-caves-garages-jardins.md +8 -0
- package/examples/immeuble/sources/documents/annexes-jardins-garages.md +1 -0
- package/examples/immeuble/sources/documents/baux-erables.md +1 -0
- package/examples/immeuble/sources/documents/baux-locatifs.md +11 -0
- package/examples/immeuble/sources/documents/coda-janvier-2026.md +10 -0
- package/examples/immeuble/sources/documents/composition-menages.md +1 -0
- package/examples/immeuble/sources/documents/composition-occupants.md +18 -0
- package/examples/immeuble/sources/documents/expected-coverage.json +70 -0
- package/examples/immeuble/sources/documents/extrait-coda-janvier-2026.md +1 -0
- package/examples/immeuble/sources/documents/groupes-facturation.md +30 -0
- package/examples/immeuble/sources/documents/manifest.json +79 -0
- package/examples/immeuble/sources/documents/note-architecte-chantier-erables.md +3 -0
- package/examples/immeuble/sources/documents/permis-urbanisme-erables.md +3 -0
- package/examples/immeuble/sources/documents/procedures-operationnelles.md +3 -0
- package/examples/immeuble/sources/documents/pv-ag-budget-2026.md +1 -0
- package/examples/immeuble/sources/documents/pv-reception-reserves-erables.md +3 -0
- package/examples/immeuble/sources/documents/registre-coproprietaires.md +24 -0
- package/examples/immeuble/sources/documents/reglement-copropriete-tilleuls.md +1 -0
- package/examples/immeuble/sources/documents/statuts-erables.md +22 -0
- package/examples/immeuble/sources/documents/statuts-tilleuls.md +19 -0
- package/examples/immeuble/sources/documents/succession-jean-dupont.md +3 -0
- package/examples/immeuble/sources/documents/titre-propriete-tilleuls-a3.md +1 -0
- package/examples/immeuble/sources/documents/trous-pedagogiques.md +3 -0
- package/examples/immeuble/sources/ontology/README.md +1 -0
- package/examples/immeuble/sources/ontology/core.yaml +444 -0
- package/examples/immeuble/success-criteria.yaml +35 -0
- package/fixtures/immeuble-demo.sqlite +0 -0
- package/package.json +11 -2
- package/scripts/lib/sqlite-runtime.mjs +1 -1
- package/scripts/load-immeuble-demo.sh +103 -0
- package/scripts/seed-immeuble-projections.mjs +75 -148
- package/scripts/verify-immeuble-demo-sources.mjs +93 -0
- package/scripts/verify-immeuble-demo.mjs +69 -0
- package/build/client/_app/immutable/chunks/D0UIlUGZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/D0UIlUGZ.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CR-imLox.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CR-imLox.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DV-AjeAB.js +0 -1
- package/build/client/_app/immutable/entry/start.DV-AjeAB.js.br +0 -2
- package/build/client/_app/immutable/entry/start.DV-AjeAB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.CTEedoSY.js.br +0 -0
- 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
|
+
}
|