abelien-harness 0.17.0 → 0.18.0

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 (47) hide show
  1. package/README.md +3 -0
  2. package/package.json +1 -1
  3. package/skills/abelien-hardware/SKILL.md +2 -0
  4. package/skills/abelien-hardware/contract/host-tools.generated.json +306 -6
  5. package/skills/abelien-hardware/contract/search-index-format.md +10 -6
  6. package/skills/abelien-hardware/references/actions/hardware.selection-commit.generated.md +2 -2
  7. package/skills/abelien-hardware/references/actions/hardware.selection-propose.generated.md +2 -2
  8. package/skills/abelien-hardware/references/actions/hardware.selection-recall.generated.md +2 -2
  9. package/skills/abelien-hardware/references/selection-service-contract.md +3 -3
  10. package/skills/abelien-hardware/schemas/hardware-selection.v2.json +9 -3
  11. package/skills/abelien-hardware/scripts/dimensions.mjs +39 -19
  12. package/skills/abelien-hardware/scripts/oss-wiki-search.mjs +2 -2
  13. package/skills/abelien-hardware/scripts/selection-propose.mjs +12 -7
  14. package/skills/abelien-hardware/scripts/selection-recall.mjs +8 -7
  15. package/skills/abelien-hardware/scripts/selection-structure.mjs +8 -3
  16. package/skills/abelien-harness/contracts/schemas/cross-domain.v1.json +18 -0
  17. package/skills/abelien-harness/scripts/ab.mjs +1 -1
  18. package/skills/abelien-structure/SKILL.md +2 -0
  19. package/skills/abelien-structure/contract/host-tools.generated.json +308 -25
  20. package/skills/abelien-structure/references/actions/cad.fallback.generated.md +2 -2
  21. package/skills/abelien-structure/references/actions/structure.assets-request.generated.md +1 -1
  22. package/skills/abelien-structure/references/actions/structure.layout-check.generated.md +20 -1
  23. package/skills/abelien-structure/references/actions/structure.layout-request.generated.md +21 -2
  24. package/skills/abelien-structure/references/actions/structure.prelude.generated.md +1 -1
  25. package/skills/abelien-structure/references/actions/structure.swap-candidates.generated.md +20 -1
  26. package/skills/abelien-structure/schemas/layout-request-input.v1.json +5 -6
  27. package/skills/abelien-structure/schemas/layout-request.v1.json +3 -1
  28. package/skills/abelien-structure/schemas/skeleton.v1.json +1 -1
  29. package/skills/abelien-structure/schemas/swap-candidates-input.v1.json +3 -1
  30. package/skills/abelien-structure/scripts/layout.mjs +6 -2
  31. package/skills/abelien-structure/scripts/swap.mjs +1 -1
  32. package/skills/abelien-structure-worker/SKILL.md +13 -0
  33. package/skills/abelien-structure-worker/domain.json +1 -1
  34. package/skills/abelien-structure-worker/worker/cli-adapter.mjs +4 -2
  35. package/skills/abelien-structure-worker/worker/component-target-image.d.mts +1 -1
  36. package/skills/abelien-structure-worker/worker/component-target-image.mjs +4 -3
  37. package/skills/abelien-structure-worker/worker/eaas-relay.md +17 -0
  38. package/skills/abelien-structure-worker/worker/eaas-relay.mjs +57 -0
  39. package/skills/abelien-structure-worker/worker/identity.mjs +51 -7
  40. package/skills/abelien-structure-worker/worker/linux-launcher.py +161 -0
  41. package/skills/abelien-structure-worker/worker/linux-requirements.lock +56 -0
  42. package/skills/abelien-structure-worker/worker/linux-runtime-install.mjs +30 -0
  43. package/skills/abelien-structure-worker/worker/linux.mjs +72 -0
  44. package/skills/abelien-structure-worker/worker/main.mjs +3 -2
  45. package/skills/abelien-structure-worker/worker/supervisor.mjs +3 -2
  46. package/skills/abelien-workspace/contract/host-tools.generated.json +51 -1
  47. package/skills/abelien-workspace/references/actions/workspace.hardware-reselect-input.generated.md +2 -2
@@ -23,8 +23,8 @@
23
23
  "pattern": "^sha256:[a-f0-9]{64}$"
24
24
  },
25
25
  "Basis": {
26
- "description": "事实依据:manufacturer 厂商资料(关联具体型号行、页面或图纸位置);measured 实测(关联测量记录、被测对象与方法);estimate 估算;engineering_design 工程设计决定(预留、壁厚、假设尺寸,不能回写成厂商规格);unknown 来源或可信度不明。",
27
- "enum": ["manufacturer", "measured", "estimate", "engineering_design", "unknown"]
26
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
27
+ "enum": ["manufacturer", "measured", "estimate", "engineering_design", "recorded", "unknown"]
28
28
  },
29
29
  "Unit": {
30
30
  "description": "受控单位。px 用来如实记录没有尺度依据的读图结果,不能当毫米用;none 用于无量纲值或结构化记录。",
@@ -77,11 +77,17 @@
77
77
  "then": { "properties": { "locator": { "type": "object" } } }
78
78
  },
79
79
  {
80
- "if": { "properties": { "unit": { "enum": ["mm", "deg"] } }, "required": ["unit"] },
80
+ "if": { "properties": { "unit": { "enum": ["mm", "deg"] }, "property": {"not": {"const": "partial_outline_size"}} }, "required": ["unit"] },
81
81
  "then": {
82
82
  "required": ["frame"],
83
83
  "properties": { "value": { "oneOf": [{ "type": "number" }, { "type": "array", "items": { "type": "number" }, "minItems": 1, "maxItems": 3 }] } }
84
84
  }
85
+ },
86
+ {
87
+ "if": {"properties": {"property": {"const": "partial_outline_size"}}, "required": ["property"]},
88
+ "then": {"required": ["frame"], "properties": {"unit": {"const": "mm"},
89
+ "subject": {"enum": ["device_outline", "module_outline"]},
90
+ "value": {"$ref": "../../abelien-harness/contracts/schemas/cross-domain.v1.json#/$defs/PartialPositiveTriple"}}}
85
91
  }
86
92
  ]
87
93
  },
@@ -6,6 +6,15 @@ const nonnegative = v => typeof v === 'number' && Number.isFinite(v) && v >= 0;
6
6
  const text = v => typeof v === 'string' && v.trim().length > 0;
7
7
  const hash = v => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
8
8
  const tuple = v => Array.isArray(v) && v.length === 3 && v.every(positive);
9
+ const partialTuple = v => Array.isArray(v) && v.length === 3 && v.some(positive) && v.every(axis => axis === null || positive(axis));
10
+ const DATA_FIELDS = ['x_mm', 'y_mm', 'z_mm', 'source_ref'];
11
+ const SNAPSHOT_V2 = 'abelien.component-dimensions.v2';
12
+ /** Availability only: axes and a reference, with an explicit denominator; no confidence state. */
13
+ export function dimensionCompleteness(value, sourceRef) {
14
+ const present = [0, 1, 2].map(axis => Array.isArray(value) && value.length <= 3 && positive(value[axis])).concat(text(sourceRef));
15
+ const missing = DATA_FIELDS.filter((_, index) => !present[index]);
16
+ return { complete: missing.length === 0, filled: present.filter(Boolean).length, total: DATA_FIELDS.length, missing };
17
+ }
9
18
  const invalid = message => { throw Object.assign(new Error(message), { code: 'VALIDATION' }); };
10
19
  function keys(value, required, optional, label) {
11
20
  if (!object(value) || required.some(key => !Object.hasOwn(value, key)) || Object.keys(value).some(key => ![...required, ...optional].includes(key))) invalid(`${label} has missing or unsupported fields`);
@@ -47,31 +56,35 @@ export function validateRequirementEnvelopes(requirements) {
47
56
  for (const row of requirements?.limits ?? []) if (row?.envelope !== undefined) validateEnvelope(row.envelope);
48
57
  }
49
58
  const BASE_FIELDS = ['component_id', 'dimensions_mm', 'dimension_basis', 'dimension_geometry'];
50
- function dimensionRecord(value) {
59
+ function dimensionRecord(value, v2 = Object.hasOwn(value, 'data_completeness')) {
51
60
  const record = Object.fromEntries(BASE_FIELDS.map(key => [key, value[key]]));
52
61
  if (value.evidence_ref !== undefined) record.evidence_ref = value.evidence_ref;
62
+ if (v2) record.data_completeness = value.data_completeness;
53
63
  return record;
54
64
  }
55
- function validRecord(value) {
56
- return object(value) && text(value.component_id) && (value.dimensions_mm === null || tuple(value.dimensions_mm))
57
- && ['measured', 'datasheet', 'estimate', 'unknown'].includes(value.dimension_basis)
58
- && ['solid_box', 'bounding_box', 'unknown'].includes(value.dimension_geometry)
65
+ function validRecord(value, v2 = false) {
66
+ return object(value) && text(value.component_id) && (value.dimensions_mm === null || (v2 ? partialTuple(value.dimensions_mm) : tuple(value.dimensions_mm)))
67
+ && (v2 ? ['measured', 'datasheet', 'estimate', 'catalog'] : ['measured', 'datasheet', 'estimate', 'unknown']).includes(value.dimension_basis)
68
+ && (v2 ? ['solid_box', 'bounding_box', null] : ['solid_box', 'bounding_box', 'unknown']).includes(value.dimension_geometry)
59
69
  && (value.evidence_ref === undefined || text(value.evidence_ref))
60
- && (value.dimensions_mm !== null || (value.dimension_basis === 'unknown' && value.dimension_geometry === 'unknown'));
70
+ && (v2 ? canonical(value.data_completeness) === canonical(dimensionCompleteness(value.dimensions_mm, value.evidence_ref))
71
+ && (tuple(value.dimensions_mm) || value.dimension_basis === 'catalog' && value.dimension_geometry === null)
72
+ : value.dimensions_mm !== null || (value.dimension_basis === 'unknown' && value.dimension_geometry === 'unknown'));
61
73
  }
62
74
  // Same revision the component database export writes, for a record that did not come from a snapshot file.
63
75
  export function dimensionRevision(value) {
64
76
  const record = dimensionRecord(value);
65
- if (!validRecord(record)) invalid('component dimension record is invalid');
77
+ if (!validRecord(record, Object.hasOwn(record, 'data_completeness'))) invalid('component dimension record is invalid');
66
78
  return digest(record);
67
79
  }
68
80
  export function validateComponentSnapshot(snapshot) {
69
81
  keys(snapshot, ['schema', 'snapshot_id', 'records'], [], 'component snapshot');
70
- if (snapshot.schema !== 'abelien.component-dimensions.v1' || !Array.isArray(snapshot.records) || !hash(snapshot.snapshot_id)) invalid('unsupported component dimension snapshot');
82
+ const v2 = snapshot.schema === SNAPSHOT_V2;
83
+ if (!v2 && snapshot.schema !== 'abelien.component-dimensions.v1' || !Array.isArray(snapshot.records) || !hash(snapshot.snapshot_id)) invalid('unsupported component dimension snapshot');
71
84
  let previous;
72
85
  for (const row of snapshot.records) {
73
- keys(row, [...BASE_FIELDS, 'revision'], ['evidence_ref'], 'component snapshot record');
74
- if (!validRecord(row) || !hash(row.revision) || row.revision !== digest(dimensionRecord(row))) invalid('component dimension record or revision is invalid');
86
+ keys(row, [...BASE_FIELDS, 'revision', ...(v2 ? ['data_completeness'] : [])], ['evidence_ref'], 'component snapshot record');
87
+ if (!validRecord(row, v2) || !hash(row.revision) || row.revision !== digest(dimensionRecord(row, v2))) invalid('component dimension record or revision is invalid');
75
88
  if (previous !== undefined && previous >= row.component_id) invalid('component records must be unique and sorted by component_id');
76
89
  previous = row.component_id;
77
90
  }
@@ -82,23 +95,29 @@ export function validateComponentSnapshot(snapshot) {
82
95
  export function dimensionFields(value) {
83
96
  const source = value?.dimension_source;
84
97
  const id = value?.component_id ?? value?.record_id;
85
- const validSize = tuple(value?.dimensions_mm);
86
- const fields = { dimensions_mm: validSize ? [...value.dimensions_mm] : null, dimension_basis: 'unknown', dimension_geometry: 'unknown', dimension_source: null };
98
+ const validSize = tuple(value?.dimensions_mm) || partialTuple(value?.dimensions_mm);
99
+ const fields = { dimensions_mm: validSize ? [...value.dimensions_mm] : null, dimension_basis: 'catalog', dimension_geometry: validSize && tuple(value.dimensions_mm) ? 'bounding_box' : null, dimension_source: null,
100
+ data_completeness: dimensionCompleteness(validSize ? value.dimensions_mm : null, source?.evidence_ref) };
87
101
  if (!object(source) || source.component_id !== id || !hash(source.revision) || !hash(source.snapshot_id)) return fields;
102
+ const v2 = source.record_schema === SNAPSHOT_V2;
88
103
  const record = { component_id: id, dimensions_mm: fields.dimensions_mm, dimension_basis: value.dimension_basis,
89
- dimension_geometry: value.dimension_geometry ?? 'unknown', ...(source.evidence_ref !== undefined ? { evidence_ref: source.evidence_ref } : {}) };
104
+ dimension_geometry: v2 ? value.dimension_geometry : value.dimension_geometry ?? 'unknown', ...(source.evidence_ref !== undefined ? { evidence_ref: source.evidence_ref } : {}),
105
+ ...(v2 ? { data_completeness: value.data_completeness } : {}) };
90
106
  // Missing geometry is a legacy gap, not permission to apply solid-box predicates.
91
107
  if (value.dimension_geometry === undefined) { fields.dimension_source = { ...source }; return fields; }
92
- if (!validRecord(record) || source.revision !== digest(record)) invalid('dimension source revision does not match candidate fields');
93
- return { dimensions_mm: record.dimensions_mm, dimension_basis: record.dimension_basis, dimension_geometry: record.dimension_geometry, dimension_source: { ...source } };
108
+ if (!validRecord(record, v2) || source.revision !== digest(record)) invalid('dimension source revision does not match candidate fields');
109
+ return { dimensions_mm: record.dimensions_mm, dimension_basis: record.dimension_basis, dimension_geometry: record.dimension_geometry, dimension_source: { ...source },
110
+ data_completeness: dimensionCompleteness(record.dimensions_mm, record.evidence_ref) };
94
111
  }
95
112
  export function fieldsFromSnapshotRecord(record, snapshotId) {
96
113
  return { dimensions_mm: record.dimensions_mm, dimension_basis: record.dimension_basis, dimension_geometry: record.dimension_geometry,
97
- dimension_source: { component_id: record.component_id, revision: record.revision, snapshot_id: snapshotId, ...(record.evidence_ref !== undefined ? { evidence_ref: record.evidence_ref } : {}) } };
114
+ dimension_source: { component_id: record.component_id, revision: record.revision, snapshot_id: snapshotId, ...(Object.hasOwn(record, 'data_completeness') ? { record_schema: SNAPSHOT_V2 } : {}), ...(record.evidence_ref !== undefined ? { evidence_ref: record.evidence_ref } : {}) },
115
+ data_completeness: dimensionCompleteness(record.dimensions_mm, record.evidence_ref) };
98
116
  }
99
117
  // Compare raw dimension-bearing fields too: normalizing unknowns must not hide index/Wiki drift.
100
118
  export function sameDimensionMetadata(a, b) {
101
- const pick = value => ({ dimensions_mm: value.dimensions_mm ?? null, dimension_basis: value.dimension_basis ?? 'unknown', dimension_geometry: value.dimension_geometry ?? 'unknown', dimension_source: value.dimension_source ?? null });
119
+ const pick = value => ({ dimensions_mm: value.dimensions_mm ?? null, dimension_basis: value.dimension_basis ?? 'catalog', dimension_geometry: value.dimension_geometry ?? null, dimension_source: value.dimension_source ?? null,
120
+ data_completeness: value.data_completeness ?? dimensionCompleteness(value.dimensions_mm, value.dimension_source?.evidence_ref) });
102
121
  return canonical(pick(a)) === canonical(pick(b));
103
122
  }
104
123
  function cavity(envelope, wall) {
@@ -131,7 +150,8 @@ export function evaluateDimensions(candidate, requirements, slot) {
131
150
  .map(row => ({ id: row.id, envelope: row.envelope, hardLimit: row.source === 'user' && row.strength === 'hard' })),
132
151
  ...(fit ? [{ id: `fit:${slot.slot_id}`, envelope: fit.envelope, hardLimit: false, decides: fit.basis === 'locked', basis: fit.basis, ...(fit.zone ? { zone: fit.zone } : {}) }] : [])];
133
152
  if (!limits.length) return unknown('PRODUCT_ENVELOPE_MISSING');
134
- if (!fields.dimensions_mm || fields.dimension_basis === 'unknown' || fields.dimension_geometry === 'unknown') return unknown('COMPONENT_DIMENSIONS_UNKNOWN');
153
+ if (!tuple(fields.dimensions_mm)) return { status: 'incomplete', decision_required: false,
154
+ completeness: fields.data_completeness, reasons: [{ code: 'COMPONENT_DIMENSIONS_INCOMPLETE', missing: fields.data_completeness.missing.filter(field => field !== 'source_ref') }], method };
135
155
  const dims = fields.dimensions_mm;
136
156
  if (!positive(dims.reduce((a, b) => a * b, 1)) || !positive(Math.hypot(...dims))
137
157
  || limits.some(row => { const c = cavity(row.envelope, row.envelope.wall_mm?.minimum ?? 0); return !positive(c.volume) || !positive(c.chord); }))
@@ -194,7 +214,7 @@ export function assemblyOccupancy(models, requirements, policy = {}) {
194
214
  if (seen.has(model.bom_item_id)) continue;
195
215
  if (model.bom_item_id) seen.add(model.bom_item_id);
196
216
  const fields = dimensionFields(model);
197
- if (!fields.dimensions_mm || fields.dimension_basis === 'unknown' || !Number.isSafeInteger(model.quantity) || model.quantity < 1) { unknown_lines++; continue; }
217
+ if (!tuple(fields.dimensions_mm) || !Number.isSafeInteger(model.quantity) || model.quantity < 1) { unknown_lines++; continue; }
198
218
  const volume = fields.dimensions_mm.reduce((a, b) => a * b, model.quantity);
199
219
  if (!positive(volume) || !Number.isFinite(estimated_volume_mm3 + volume)) { unknown_lines++; continue; }
200
220
  estimated_volume_mm3 += volume;
@@ -87,7 +87,7 @@ export function parseHardwareWiki(content) {
87
87
  "schema", "type", "title", "description", "manufacturer", "model", "aliases", "resource_urn", "record_id",
88
88
  "record_version", "status", "category_path", "category_leaf", "primary_function", "integration_level",
89
89
  "hardware_form", "component_kind", "functional_capabilities", "capability_coverage", "source_url", "source_updated_at",
90
- "dimensions_mm", "dimension_basis", "dimension_geometry", "dimension_source", "interfaces", "electrical", "electrical_source",
90
+ "dimensions_mm", "dimension_basis", "dimension_geometry", "dimension_source", "data_completeness", "interfaces", "electrical", "electrical_source",
91
91
  ];
92
92
  const meta = {};
93
93
  for (const key of wanted) {
@@ -95,7 +95,7 @@ export function parseHardwareWiki(content) {
95
95
  // An empty value (including YAML block style) is treated as absent, as before these keys were read.
96
96
  const line = frontMatter.match(new RegExp(`^${key}:[ \\t]*(.*)$`, "m"));
97
97
  if (line && line[1].trim()) {
98
- if (["dimension_source", "dimensions_mm", "electrical", "electrical_source"].includes(key)) {
98
+ if (["dimension_source", "dimensions_mm", "data_completeness", "electrical", "electrical_source"].includes(key)) {
99
99
  try { meta[key] = JSON.parse(line[1]); }
100
100
  catch { throw error("WIKI_INVALID", `${key} must be generated inline JSON`); }
101
101
  } else meta[key] = scalar(line[1]);
@@ -3,7 +3,7 @@
3
3
  // candidate set; the other slots carry their lines unchanged. Dimensions and electrical are recomputed for the whole
4
4
  // set with the same deterministic evaluators hardware.select uses, plus board-side pin-to-pin interfaces. Checks never
5
5
  // decide the gate. from_selection bridges a stored v1 selection (hardware.select) into the same model.
6
- import { assemblyOccupancy, evaluateDimensions } from "./dimensions.mjs";
6
+ import { assemblyOccupancy, dimensionCompleteness, evaluateDimensions } from "./dimensions.mjs";
7
7
  import { evaluateElectrical } from "./electrical.mjs";
8
8
  import { selectionLineId } from "./finalize-selection.mjs";
9
9
  import {
@@ -17,7 +17,7 @@ import { sliceCodePoints } from "../../abelien-harness/scripts/web/safe-text.mjs
17
17
 
18
18
  // Fact bases mapped onto the component-record bases the shared evaluators understand. Engineering decisions and
19
19
  // estimates never gain datasheet authority.
20
- const RECORD_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", unknown: "unknown" };
20
+ const RECORD_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", recorded: "unknown", unknown: "unknown" };
21
21
  const ELECTRICAL_STATUS = { pass: "pass", fail: "conflict", gap: "conflict", risk: "unknown", unknown: "unknown" };
22
22
  const RANK = { pass: 0, unchecked: 1, unknown: 2, conflict: 3 };
23
23
  const NO_SNAPSHOT = "0".repeat(64);
@@ -55,15 +55,20 @@ function sizeRecord(componentId, fact) {
55
55
  return { ...fields, dimension_source: { component_id: componentId, revision: digest(fields), snapshot_id: fact.source_ref?.digest ?? NO_SNAPSHOT } };
56
56
  }
57
57
  function dimensionItem(slot, candidate, requirements, requirementsRef) {
58
- const base = { item_id: refId("dimensions:", slot.slot_id), subjects: [slot.slot_id] };
58
+ const picked = candidate ? pickOutline(candidate) : {};
59
+ const partial = candidate?.facts.find(fact => fact.property === "partial_outline_size" && fact.unit === "mm" && OUTLINE_SUBJECTS.includes(fact.subject));
60
+ const data_completeness = dimensionCompleteness(picked.fact?.value ?? partial?.value,
61
+ picked.fact?.source_ref?.id ?? partial?.source_ref?.id ?? picked.refs?.[0]?.id);
62
+ const base = { item_id: refId("dimensions:", slot.slot_id), subjects: [slot.slot_id], detail: {data_completeness} };
59
63
  if (!candidate) return { ...base, status: "unchecked", method: "none", evidence_refs: [], reason: "SLOT_UNFILLED" };
60
64
  if (!requirements.limits.some(row => row?.envelope) && !slot.fit_envelope) return { ...base, status: "unchecked", method: "none", evidence_refs: [], reason: "NO_ENVELOPE_LIMIT" };
61
- const picked = pickOutline(candidate);
62
- if (!picked.fact) return { ...base, status: "unknown", method: "computed", evidence_refs: picked.refs, reason: picked.reason };
65
+ if (!picked.fact) return { ...base, status: "unchecked", method: "none", evidence_refs: [], reason: picked.reason };
63
66
  const check = evaluateDimensions(sizeRecord(candidate.component_id ?? candidate.candidate_id, picked.fact), requirements, slot);
64
67
  const [status, reason] = check.status === "incompatible" ? ["conflict", "DIMENSION_INCOMPATIBLE"] : check.status === "risk" ? ["unknown", "DIMENSION_RISK"]
65
- : check.status === "clear" ? trustedFact(picked.fact) ? ["pass", "DIMENSION_CLEAR"] : ["unknown", "ESTIMATED_SIZE_ONLY"] : ["unknown", check.reasons[0]?.code ?? "DIMENSION_UNKNOWN"];
66
- return { ...base, status, method: "computed", evidence_refs: uniqueRefs([picked.fact.source_ref, requirementsRef]), reason, detail: { fact_id: picked.fact.fact_id, dimension_check: check } };
68
+ : check.status === "clear" ? ["pass", "DIMENSION_CLEAR"] : ["unchecked", check.reasons[0]?.code ?? "DIMENSION_INCOMPLETE"];
69
+ return { ...base, status, method: status === "unchecked" ? "none" : "computed",
70
+ evidence_refs: status === "unchecked" ? [] : uniqueRefs([picked.fact.source_ref, requirementsRef]), reason,
71
+ detail: { data_completeness, fact_id: picked.fact.fact_id, dimension_check: check } };
67
72
  }
68
73
 
69
74
  // Board-side required_interfaces against each target slot's interfaces: pass or conflict only when both facts are trusted.
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  const UNSPECIFIED_MANUFACTURER = "Unspecified";
15
15
  // Component database bases map onto fact bases; datasheet values are the manufacturer's.
16
- const FACT_BASIS = { datasheet: "manufacturer", measured: "measured", estimate: "estimate", unknown: "unknown" };
16
+ const FACT_BASIS = { datasheet: "manufacturer", measured: "measured", estimate: "estimate", catalog: "recorded", unknown: "recorded" };
17
17
  const LOCAL_FRAME = { coordinate_system: "component_local", datum: "unspecified", orientation: "unspecified", tolerance_mm: null };
18
18
  const candidateId = (slotId, kind, key) => `cand-${digest({ slot_id: slotId, item_kind: kind, key }).slice(0, 24)}`;
19
19
  const normalized = value => value.toLowerCase().replace(/[\s_]+/g, "");
@@ -28,16 +28,17 @@ function recordFacts(part, component, addEvidence) {
28
28
  let dims;
29
29
  try { dims = dimensionFields(part); } catch (error) { integrity(`candidate ${component} dimension source does not match its fields`, { cause: error.message }); }
30
30
  if (dims.dimensions_mm) {
31
+ const property = dims.dimensions_mm.every(value => typeof value === "number") ? "outline_size" : "partial_outline_size";
31
32
  if (dims.dimension_source) {
32
33
  const ref = { id: refId("dimension-record:", component), version: `r-${dims.dimension_source.revision.slice(0, 12)}`, digest: dims.dimension_source.revision };
33
34
  addEvidence({ ref, kind: "dimension_record", content: null, content_sha256: null, locator: { snapshot_id: dims.dimension_source.snapshot_id } });
34
- facts.push({ fact_id: "outline_size", property: "outline_size", subject: "device_outline", value: dims.dimensions_mm, unit: "mm",
35
+ facts.push({ fact_id: property, property, subject: "device_outline", value: dims.dimensions_mm, unit: "mm",
35
36
  basis: FACT_BASIS[dims.dimension_basis], source_ref: ref,
36
37
  locator: { snapshot_id: dims.dimension_source.snapshot_id, field: "dimensions_mm", ...(dims.dimension_source.evidence_ref ? { evidence_ref: dims.dimension_source.evidence_ref } : {}) },
37
38
  conditions: {}, frame: LOCAL_FRAME, method: "component_dimension_record",
38
39
  ...(["solid_box", "bounding_box"].includes(dims.dimension_geometry) ? { shape: dims.dimension_geometry } : {}) });
39
- } else facts.push({ fact_id: "outline_size", property: "outline_size", subject: "device_outline", value: dims.dimensions_mm, unit: "mm",
40
- basis: "unknown", source_ref: null, locator: { field: "dimensions_mm", binding: "legacy_unbound" }, conditions: {}, frame: LOCAL_FRAME });
40
+ } else facts.push({ fact_id: property, property, subject: "device_outline", value: dims.dimensions_mm, unit: "mm",
41
+ basis: "recorded", source_ref: null, locator: { field: "dimensions_mm", binding: "legacy_unbound" }, conditions: {}, frame: LOCAL_FRAME });
41
42
  }
42
43
  // Electrical: the component-electrical record as one structured fact, only when it is source-bound.
43
44
  let electrical;
@@ -51,7 +52,7 @@ function recordFacts(part, component, addEvidence) {
51
52
  }
52
53
  // Interfaces written in a library record have no source binding yet: kept for display, never trusted.
53
54
  if (validInterfaceList(part.interfaces)) facts.push({ fact_id: "interfaces", property: "interfaces", subject: "connector", value: part.interfaces.map(({ type, package: pkg }) => ({ type, package: pkg })),
54
- unit: "none", basis: "unknown", source_ref: null, locator: { field: "interfaces", binding: "legacy_unbound" }, conditions: {} });
55
+ unit: "none", basis: "recorded", source_ref: null, locator: { field: "interfaces", binding: "legacy_unbound" }, conditions: {} });
55
56
  return facts;
56
57
  }
57
58
 
@@ -76,7 +77,7 @@ function freezeLibrary(candidate, slot, claim, addEvidence, readBlob, texts) {
76
77
  const facts = recordFacts(candidate, component, addEvidence);
77
78
  // Web envelopes are source quotes of unknown authority: kept for display, never used for fitting.
78
79
  if (web && record(candidate.geometry)) facts.push({ fact_id: "source_envelope", property: "source_envelope", subject: "other", value: candidate.geometry,
79
- unit: "none", basis: "unknown", source_ref: source.ref, locator: { page_id: candidate.source?.page_id ?? "unknown" }, conditions: {} });
80
+ unit: "none", basis: "recorded", source_ref: source.ref, locator: { page_id: candidate.source?.page_id ?? "unknown" }, conditions: {} });
80
81
  // Identity: an exact ordering code needs a verbatim quote from the frozen text; otherwise the model text is only a series.
81
82
  const manufacturer = String(candidate.manufacturer ?? "");
82
83
  let model = String(candidate.model), status = !text(manufacturer) || manufacturer === UNSPECIFIED_MANUFACTURER ? "unknown"
@@ -86,7 +87,7 @@ function freezeLibrary(candidate, slot, claim, addEvidence, readBlob, texts) {
86
87
  invalid(`identity claim for ${component} needs a verbatim quote from its frozen text that contains an ordering code of model ${JSON.stringify(model)}`);
87
88
  if (status !== "unknown") { status = "exact_model_confirmed"; model = claim.ordering_code; }
88
89
  facts.push({ fact_id: "ordering_code", property: "ordering_code", subject: "device", value: claim.ordering_code, unit: "none",
89
- basis: web ? "unknown" : "manufacturer", source_ref: source.ref, locator: { quote: claim.quote }, conditions: {}, method: "verbatim_quote" });
90
+ basis: web ? "recorded" : "manufacturer", source_ref: source.ref, locator: { quote: claim.quote }, conditions: {}, method: "verbatim_quote" });
90
91
  }
91
92
  const issues = web ? ["web source authority and engineering data are unverified; geometry is an envelope from source quotes"] : [];
92
93
  return { candidate: { candidate_id: candidateId(slot.slot_id, "purchased", component), slot_id: slot.slot_id, item_kind: "purchased",
@@ -3,8 +3,9 @@
3
3
  // interfaces and facing axes: structure takes them as the part's own data, so anything else stays null or absent and
4
4
  // structure records unknown. Sizes keep their basis, so estimates still only demote.
5
5
  import { OUTLINE_SUBJECTS, digest, factOf, identityOf, invalid, refId, trustedFact, verifySelectionRevision } from "./selection-model.mjs";
6
+ import { dimensionCompleteness } from "./dimensions.mjs";
6
7
 
7
- const SIZE_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", unknown: "unknown" };
8
+ const SIZE_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", recorded: "catalog", unknown: "catalog" };
8
9
 
9
10
  /** The frozen part object behind a candidate: its identity plus its facts, versioned by the identity's own version. */
10
11
  export function componentRefOf(candidate) {
@@ -16,8 +17,12 @@ export function componentRefOf(candidate) {
16
17
  function size(candidate) {
17
18
  const outline = (candidate.facts ?? []).filter(fact => fact.property === "outline_size" && OUTLINE_SUBJECTS.includes(fact.subject) && fact.unit === "mm");
18
19
  const fact = outline.find(trustedFact) ?? outline[0];
19
- const basis = fact ? SIZE_BASIS[fact.basis] : "unknown";
20
- return { dimensions_mm: basis === "unknown" ? null : [...fact.value], dimension_basis: basis };
20
+ const basis = fact ? SIZE_BASIS[fact.basis] : "catalog";
21
+ const value = fact ? [...fact.value] : null;
22
+ const partial = !fact && candidate.facts.find(item => item.property === "partial_outline_size" && item.unit === "mm" && OUTLINE_SUBJECTS.includes(item.subject));
23
+ return { dimensions_mm: value, dimension_basis: basis,
24
+ ...(partial ? {partial_dimensions_mm: [...partial.value]} : {}),
25
+ data_completeness: dimensionCompleteness(value ?? partial?.value, fact?.source_ref?.id ?? partial?.source_ref?.id) };
21
26
  }
22
27
  const trustedValue = (candidate, property) => { const fact = factOf(candidate, property); return fact && trustedFact(fact) ? fact.value : null; };
23
28
 
@@ -4,6 +4,18 @@
4
4
  "title": "abelien.contracts.cross-domain.v1",
5
5
  "description": "第 10 组:跨域产物(流程策略 §5、§10,硬件证据模型 §3)。钥匙是需求的 reaction_id,往下每层多一个字段,不另起键:槽位带 reaction_ids,BOM 行带 slot_id,点位带 binding{reaction_id, slot_id},骨架给每个物理实例一个 occurrence。数值次序(min ≤ max、壁厚留出空腔)等跨字段算术仍由各域的确定性检查负责。",
6
6
  "$defs": {
7
+ "DataCompleteness": {
8
+ "description": "资料可用项覆盖率;只统计完整与缺项,不评价来源可信度。",
9
+ "type": "object",
10
+ "required": ["complete", "filled", "total", "missing"],
11
+ "properties": {
12
+ "complete": { "type": "boolean" },
13
+ "filled": { "type": "integer", "minimum": 0 },
14
+ "total": { "type": "integer", "minimum": 1 },
15
+ "missing": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
16
+ },
17
+ "additionalProperties": false
18
+ },
7
19
  "ShortIdentifier": {
8
20
  "description": "分区名等短标识(与现有 fit_envelope.zone 规则相同)。",
9
21
  "type": "string",
@@ -15,6 +27,12 @@
15
27
  "minItems": 3,
16
28
  "maxItems": 3
17
29
  },
30
+ "PartialPositiveTriple": {
31
+ "type": "array",
32
+ "items": {"oneOf": [{"type": "number", "exclusiveMinimum": 0}, {"type": "null"}]},
33
+ "minItems": 3,
34
+ "maxItems": 3
35
+ },
18
36
  "Triple": {
19
37
  "type": "array",
20
38
  "items": { "type": "number" },
@@ -25,7 +25,7 @@ export { interpolate, findWorkflow, loadWorkflow, workflowDefinitionDigest, pars
25
25
  // doctor must report the shipped package version, never a frozen constant (0.2.3 drifted across
26
26
  // six releases). The npm tree always ships package.json at the skill root; standalone skill copies
27
27
  // fall back to FALLBACK_VERSION, which scripts/release-check.mjs pins to package.json.
28
- const FALLBACK_VERSION = "0.17.0";
28
+ const FALLBACK_VERSION = "0.18.0";
29
29
  export let VERSION = FALLBACK_VERSION;
30
30
  try {
31
31
  VERSION = String(JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || FALLBACK_VERSION);
@@ -62,3 +62,5 @@ CAD 只经宿主的受管工具执行:`cad.assets` 取组件 CAD,`cad.preche
62
62
  | [结构合理性规则](references/structure-reasonableness.md) | 判断结构合不合理、写自检表,或 remix 直接建模时 |
63
63
  | [交接契约](references/handoff.md) | 首次参与或基线变化,需要知道输入、反馈与结果的语义时 |
64
64
  | [动作参数与错误手册](references/actions.generated.md) | 调用任何动作前核对参数与错误码时 |
65
+
66
+ 尺寸资料按 data_completeness 显示完整度;库内已有三轴直接参与布局,不因来源标签 unknown 丢弃。catalog 是资料来源类型,实际缺轴时才要求 reserved_mm。规则见 [尺寸完整度契约](../abelien-hardware/references/selection-service-contract.md)。