abelien-harness 0.8.0 → 0.9.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 (33) hide show
  1. package/.env.example +19 -20
  2. package/README.md +12 -12
  3. package/package.json +1 -1
  4. package/skills/abelien-hardware/SKILL.md +4 -4
  5. package/skills/abelien-hardware/domain.json +4 -4
  6. package/skills/abelien-hardware/references/knowledge-recall-service-contract.md +4 -4
  7. package/skills/abelien-hardware/references/search-index-format.md +5 -5
  8. package/skills/abelien-hardware/references/search-index-release.md +7 -7
  9. package/skills/abelien-hardware/references/selection-service-contract.md +3 -3
  10. package/skills/abelien-hardware/scripts/build-search-index.mjs +1 -1
  11. package/skills/abelien-hardware/scripts/oss-object-store.mjs +247 -0
  12. package/skills/abelien-hardware/scripts/{tos-wiki-search.mjs → oss-wiki-search.mjs} +19 -19
  13. package/skills/abelien-hardware/scripts/search-index-release.mjs +25 -25
  14. package/skills/abelien-hardware-selection/SKILL.md +2 -2
  15. package/skills/abelien-hardware-selection/domain.json +2 -2
  16. package/skills/abelien-hardware-selection/scripts/load-guide.mjs +1 -1
  17. package/skills/abelien-harness/references/adapter-contract.md +1 -1
  18. package/skills/abelien-harness/references/m1-integration.md +2 -2
  19. package/skills/abelien-harness/scripts/ab.mjs +8 -1
  20. package/skills/abelien-harness/scripts/studio.mjs +15 -14
  21. package/skills/abelien-reference/domain.json +1 -1
  22. package/skills/abelien-reference/references/research-contract.md +1 -0
  23. package/skills/abelien-reference/schemas/reference-evidence.v1.json +1 -1
  24. package/skills/abelien-reference/scripts/finalize.mjs +3 -1
  25. package/skills/abelien-workspace/SKILL.md +1 -1
  26. package/skills/abelien-workspace/assets/journey-stage-plan.json +46 -13
  27. package/skills/abelien-workspace/domain.json +3 -2
  28. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +2 -2
  29. package/skills/abelien-workspace/references/harness-binding.md +3 -3
  30. package/skills/abelien-workspace/references/user-journey.md +5 -5
  31. package/skills/abelien-workspace/scripts/journey-next.mjs +10 -2
  32. package/skills/abelien-workspace/workflows/user-journey.json +1 -1
  33. package/skills/abelien-hardware/scripts/tos-object-store.mjs +0 -202
@@ -2,7 +2,7 @@ import zlib from "node:zlib";
2
2
  import { dimensionFields, evaluateDimensions, orderByDimensionTier, sameDimensionMetadata, screenDimensions, shortlistByDimensionTier, validateFitEnvelope, validateRequirementEnvelopes } from "./dimensions.mjs";
3
3
  import { electricalFields } from "./electrical.mjs";
4
4
  import { selectionRequestDigest, validCategoryPath } from "./selection-request.mjs";
5
- import { createTosConfig, getTosObject, sha256 } from "./tos-object-store.mjs";
5
+ import { createOssConfig, getOssObject, sha256 } from "./oss-object-store.mjs";
6
6
 
7
7
  const SEARCH_CATALOG_SCHEMA = "abelien.hardware-wiki-search-catalog.v1";
8
8
  const SEARCH_INDEX_SCHEMA = "abelien.hardware-wiki-category-index.v1";
@@ -15,13 +15,13 @@ function error(code, message, details) {
15
15
  }
16
16
 
17
17
  async function getObject(config, key, fetchImpl, { optional = false } = {}) {
18
- const object = await getTosObject(config, key, { fetchImpl, optional });
18
+ const object = await getOssObject(config, key, { fetchImpl, optional });
19
19
  if (!object) return null;
20
20
  const bytes = object.bytes;
21
21
  let content = bytes;
22
22
  if (key.endsWith(".gz") && bytes[0] === 0x1f && bytes[1] === 0x8b) {
23
23
  try { content = zlib.gunzipSync(bytes); }
24
- catch { throw error("WIKI_SEARCH_INDEX_INVALID", "TOS category index is not valid gzip", { key }); }
24
+ catch { throw error("WIKI_SEARCH_INDEX_INVALID", "OSS category index is not valid gzip", { key }); }
25
25
  }
26
26
  return {
27
27
  text: content.toString("utf8"),
@@ -171,9 +171,9 @@ function parseJsonObject(object, code, message, key) {
171
171
  }
172
172
 
173
173
  function parseCatalog(object, key) {
174
- const catalog = parseJsonObject(object, "WIKI_SEARCH_CATALOG_INVALID", "TOS hardware search catalog is not valid JSON", key);
174
+ const catalog = parseJsonObject(object, "WIKI_SEARCH_CATALOG_INVALID", "OSS hardware search catalog is not valid JSON", key);
175
175
  if (catalog?.schema !== SEARCH_CATALOG_SCHEMA || !Array.isArray(catalog.categories)) {
176
- throw error("WIKI_SEARCH_CATALOG_INVALID", "TOS hardware search catalog has an unsupported shape", { key });
176
+ throw error("WIKI_SEARCH_CATALOG_INVALID", "OSS hardware search catalog has an unsupported shape", { key });
177
177
  }
178
178
  const categories = new Map();
179
179
  for (const descriptor of catalog.categories) {
@@ -195,9 +195,9 @@ function parseCategoryIndex(object, descriptor, key) {
195
195
  if (descriptor.sha256 && descriptor.sha256 !== object.raw_sha256) {
196
196
  throw error("WIKI_SEARCH_INDEX_INVALID", "category index SHA-256 does not match catalog", { key });
197
197
  }
198
- const index = parseJsonObject(object, "WIKI_SEARCH_INDEX_INVALID", "TOS hardware category index is not valid JSON", key);
198
+ const index = parseJsonObject(object, "WIKI_SEARCH_INDEX_INVALID", "OSS hardware category index is not valid JSON", key);
199
199
  if (index?.schema !== SEARCH_INDEX_SCHEMA || !categoryMatches(index.category_path, descriptor.category_path) || !Array.isArray(index.records)) {
200
- throw error("WIKI_SEARCH_INDEX_INVALID", "TOS hardware category index has an unsupported shape", { key });
200
+ throw error("WIKI_SEARCH_INDEX_INVALID", "OSS hardware category index has an unsupported shape", { key });
201
201
  }
202
202
  if (index.records.length !== descriptor.record_count) {
203
203
  throw error("WIKI_SEARCH_INDEX_INVALID", "category index record count does not match catalog", { key });
@@ -327,7 +327,7 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
327
327
  const results = [];
328
328
  const unmatchedSlots = [];
329
329
  const shardSnapshots = [];
330
- const concurrency = envPositiveInteger(env, "TOS_SEARCH_CONCURRENCY", 8);
330
+ const concurrency = envPositiveInteger(env, "OSS_SEARCH_CONCURRENCY", 8);
331
331
 
332
332
  async function loadIndex(descriptor) {
333
333
  if (!indexCache.has(descriptor.index_key)) {
@@ -383,7 +383,7 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
383
383
  results,
384
384
  unmatchedSlots,
385
385
  unknowns: [],
386
- sourceSnapshot: `tos:${config.bucket}:${config.prefix}:catalog:${catalogObject.etag}:indexes:${shardsDigest}:content:${catalogObject.raw_sha256}`,
386
+ sourceSnapshot: `oss:${config.bucket}:${config.prefix}:catalog:${catalogObject.etag}:indexes:${shardsDigest}:content:${catalogObject.raw_sha256}`,
387
387
  retrievalMode: "category_index",
388
388
  };
389
389
  }
@@ -391,8 +391,8 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
391
391
  async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, maxFetch, includeContent }) {
392
392
  const manifestKey = `${config.prefix}_manifest.json`;
393
393
  const manifestObject = await getObject(config, manifestKey, fetchImpl);
394
- const manifest = parseJsonObject(manifestObject, "WIKI_MANIFEST_INVALID", "TOS hardware Wiki manifest is not valid JSON", manifestKey);
395
- if (!Array.isArray(manifest)) throw error("WIKI_MANIFEST_INVALID", "TOS hardware Wiki manifest must be an array", { key: manifestKey });
394
+ const manifest = parseJsonObject(manifestObject, "WIKI_MANIFEST_INVALID", "OSS hardware Wiki manifest is not valid JSON", manifestKey);
395
+ if (!Array.isArray(manifest)) throw error("WIKI_MANIFEST_INVALID", "OSS hardware Wiki manifest must be an array", { key: manifestKey });
396
396
  const scanLimit = Number.isInteger(input.policy?.scan_limit_per_slot) && input.policy.scan_limit_per_slot > 0
397
397
  ? input.policy.scan_limit_per_slot : null;
398
398
  const wikiLoader = createWikiLoader(config, fetchImpl, { legacy: true });
@@ -405,7 +405,7 @@ async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, m
405
405
  if (scanLimit && allEntries.length > entries.length) {
406
406
  unknowns.push({ slot_id: slot.slot_id, code: "SCAN_LIMIT", available: allEntries.length, scanned: entries.length });
407
407
  }
408
- const loaded = await mapConcurrent(entries, envPositiveInteger(env, "TOS_SEARCH_CONCURRENCY", 8), wikiLoader);
408
+ const loaded = await mapConcurrent(entries, envPositiveInteger(env, "OSS_SEARCH_CONCURRENCY", 8), wikiLoader);
409
409
  const semanticQuery = semanticQueryFor(input, slot);
410
410
  const candidates = shortlistByDimensionTier(loaded.map(document => {
411
411
  const ranking = scoreDocument(document, semanticQuery, input.policy?.prefer_system_candidates !== false);
@@ -421,28 +421,28 @@ async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, m
421
421
  }
422
422
  return {
423
423
  results, unmatchedSlots, unknowns,
424
- sourceSnapshot: `tos:${config.bucket}:${config.prefix}:manifest:${manifestObject.etag}`,
424
+ sourceSnapshot: `oss:${config.bucket}:${config.prefix}:manifest:${manifestObject.etag}`,
425
425
  retrievalMode: "legacy_manifest_full_scan",
426
426
  };
427
427
  }
428
428
 
429
429
  export async function searchHardware({ input, env, fetchImpl = fetch }) {
430
430
  validateInput(input);
431
- const config = createTosConfig(env);
431
+ const config = createOssConfig(env);
432
432
  const policy = input.policy ?? {};
433
433
  const topK = Number.isInteger(policy.top_k_per_slot) && policy.top_k_per_slot > 0 ? policy.top_k_per_slot : 8;
434
- const maxFullWikiFetch = envPositiveInteger(env, "TOS_MAX_FULL_WIKI_FETCH_PER_SLOT", 24);
434
+ const maxFullWikiFetch = envPositiveInteger(env, "OSS_MAX_FULL_WIKI_FETCH_PER_SLOT", 24);
435
435
  if (topK > maxFullWikiFetch) {
436
- throw error("VALIDATION", `top_k_per_slot exceeds TOS_MAX_FULL_WIKI_FETCH_PER_SLOT (${maxFullWikiFetch})`, {
436
+ throw error("VALIDATION", `top_k_per_slot exceeds OSS_MAX_FULL_WIKI_FETCH_PER_SLOT (${maxFullWikiFetch})`, {
437
437
  top_k_per_slot: topK, max_full_wiki_fetch_per_slot: maxFullWikiFetch,
438
438
  });
439
439
  }
440
440
  const includeContent = policy.include_content !== false;
441
441
  const catalogObject = await getObject(config, `${config.prefix}_catalog.json`, fetchImpl, { optional: true });
442
- const allowLegacy = policy.allow_legacy_manifest_fallback === true || env.TOS_ALLOW_LEGACY_MANIFEST_FALLBACK === "true";
442
+ const allowLegacy = policy.allow_legacy_manifest_fallback === true || env.OSS_ALLOW_LEGACY_MANIFEST_FALLBACK === "true";
443
443
  if (!catalogObject && !allowLegacy) {
444
- throw error("WIKI_SEARCH_INDEX_MISSING", "TOS hardware category search catalog is missing; publish _catalog.json and indexes before searching", {
445
- key: `${config.prefix}_catalog.json`, migration_env: "TOS_ALLOW_LEGACY_MANIFEST_FALLBACK=true",
444
+ throw error("WIKI_SEARCH_INDEX_MISSING", "OSS hardware category search catalog is missing; publish _catalog.json and indexes before searching", {
445
+ key: `${config.prefix}_catalog.json`, migration_env: "OSS_ALLOW_LEGACY_MANIFEST_FALLBACK=true",
446
446
  });
447
447
  }
448
448
  const searched = catalogObject
@@ -3,8 +3,8 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import zlib from "node:zlib";
5
5
  import { buildSearchIndex } from "./build-search-index.mjs";
6
- import { searchHardware } from "./tos-wiki-search.mjs";
7
- import { createTosConfig, getTosObject, headTosObject, putTosObject, sha256 } from "./tos-object-store.mjs";
6
+ import { searchHardware } from "./oss-wiki-search.mjs";
7
+ import { createOssConfig, getOssObject, headOssObject, putOssObject, sha256 } from "./oss-object-store.mjs";
8
8
 
9
9
  const CATALOG_SCHEMA = "abelien.hardware-wiki-search-catalog.v1";
10
10
  const INDEX_SCHEMA = "abelien.hardware-wiki-category-index.v1";
@@ -102,11 +102,11 @@ async function mapConcurrent(items, limit, worker) {
102
102
  async function requireMatchingManifest(config, catalog, fetchImpl, { optional = false } = {}) {
103
103
  const relative = catalog.source_manifest?.object_key || "_manifest.json";
104
104
  const key = `${config.prefix}${relative}`;
105
- const object = await getTosObject(config, key, { fetchImpl, optional });
105
+ const object = await getOssObject(config, key, { fetchImpl, optional });
106
106
  if (!object) return null;
107
107
  const actual = sha256(object.bytes);
108
108
  if (actual !== catalog.source_manifest?.sha256) {
109
- throw releaseError("SOURCE_MANIFEST_MISMATCH", "TOS Wiki Manifest does not match the index source", {
109
+ throw releaseError("SOURCE_MANIFEST_MISMATCH", "OSS Wiki Manifest does not match the index source", {
110
110
  key, expected_sha256: catalog.source_manifest?.sha256, actual_sha256: actual,
111
111
  });
112
112
  }
@@ -126,7 +126,7 @@ async function bootstrapRemoteManifest({ config, catalog, sourceDir, fetchImpl,
126
126
  const localFile = safeFile(sourceDir, entry.path);
127
127
  const localSize = fs.statSync(localFile).size;
128
128
  const key = `${config.prefix}${entry.path}`;
129
- const head = await headTosObject(config, key, { fetchImpl });
129
+ const head = await headOssObject(config, key, { fetchImpl });
130
130
  if (head.contentLength !== null && head.contentLength !== localSize) {
131
131
  throw releaseError("REMOTE_WIKI_MISMATCH", "remote Wiki object size does not match local source", { key, expected_bytes: localSize, actual_bytes: head.contentLength });
132
132
  }
@@ -134,13 +134,13 @@ async function bootstrapRemoteManifest({ config, catalog, sourceDir, fetchImpl,
134
134
  if (head.metadataSha256 !== entry.sha256) throw releaseError("REMOTE_WIKI_MISMATCH", "remote Wiki metadata SHA-256 does not match Manifest", { key, expected_sha256: entry.sha256, actual_sha256: head.metadataSha256 });
135
135
  return;
136
136
  }
137
- const object = await getTosObject(config, key, { fetchImpl });
137
+ const object = await getOssObject(config, key, { fetchImpl });
138
138
  const actual = sha256(object.bytes);
139
139
  if (actual !== entry.sha256) throw releaseError("REMOTE_WIKI_MISMATCH", "remote Wiki SHA-256 does not match Manifest", { key, expected_sha256: entry.sha256, actual_sha256: actual });
140
140
  });
141
141
  const key = `${config.prefix}${catalog.source_manifest.object_key || "_manifest.json"}`;
142
142
  await putImmutable(config, key, manifestBytes, fetchImpl);
143
- const uploaded = await getTosObject(config, key, { fetchImpl });
143
+ const uploaded = await getOssObject(config, key, { fetchImpl });
144
144
  if (sha256(uploaded.bytes) !== catalog.source_manifest.sha256) {
145
145
  throw releaseError("SOURCE_MANIFEST_MISMATCH", "uploaded Wiki Manifest SHA-256 mismatch", { key });
146
146
  }
@@ -158,38 +158,38 @@ function objectContentType(key) {
158
158
  // 并发写同一键只会写入同一份字节;已存在但字节不同即冲突失败,不覆盖。
159
159
  async function putImmutable(config, key, bytes, fetchImpl) {
160
160
  const expected = sha256(bytes);
161
- const existing = await getTosObject(config, key, { fetchImpl, optional: true });
161
+ const existing = await getOssObject(config, key, { fetchImpl, optional: true });
162
162
  if (existing) {
163
163
  const actual = sha256(existing.bytes);
164
164
  if (actual !== expected) throw releaseError("IMMUTABLE_OBJECT_CONFLICT", "release object already exists with different bytes", { key, expected_sha256: expected, actual_sha256: actual });
165
165
  return { key, sha256: expected, reused: true };
166
166
  }
167
- await putTosObject(config, key, bytes, {
167
+ await putOssObject(config, key, bytes, {
168
168
  fetchImpl,
169
169
  headers: { "content-type": objectContentType(key), "x-amz-meta-sha256": expected },
170
170
  });
171
- const uploaded = await getTosObject(config, key, { fetchImpl });
171
+ const uploaded = await getOssObject(config, key, { fetchImpl });
172
172
  const actual = sha256(uploaded.bytes);
173
173
  if (actual !== expected) throw releaseError("INDEX_UPLOAD_VERIFY_FAILED", "uploaded release object SHA-256 mismatch", { key, expected_sha256: expected, actual_sha256: actual });
174
174
  return { key, sha256: expected, reused: false };
175
175
  }
176
176
 
177
177
  export async function verifyPublishedIndex({ env, fetchImpl = fetch, concurrency } = {}) {
178
- const config = createTosConfig(env);
178
+ const config = createOssConfig(env);
179
179
  const catalogKey = `${config.prefix}_catalog.json`;
180
- const catalogObject = await getTosObject(config, catalogKey, { fetchImpl });
180
+ const catalogObject = await getOssObject(config, catalogKey, { fetchImpl });
181
181
  const catalog = parseJson(catalogObject.bytes, "WIKI_SEARCH_CATALOG_INVALID", catalogKey);
182
182
  if (catalog.schema !== CATALOG_SCHEMA || !catalog.release_id || !Array.isArray(catalog.categories) || !catalog.categories.length) {
183
183
  throw releaseError("WIKI_SEARCH_CATALOG_INVALID", "published search catalog is invalid", { key: catalogKey });
184
184
  }
185
185
  const manifest = await requireMatchingManifest(config, catalog, fetchImpl);
186
- const limit = Number(concurrency || env?.TOS_PUBLISH_CONCURRENCY || 6);
186
+ const limit = Number(concurrency || env?.OSS_PUBLISH_CONCURRENCY || 6);
187
187
  const checked = await mapConcurrent(catalog.categories, Number.isFinite(limit) ? limit : 6, async descriptor => {
188
188
  if (!descriptor.index_key?.startsWith(`indexes/releases/${catalog.release_id}/`)) {
189
189
  throw releaseError("WIKI_SEARCH_CATALOG_INVALID", "published catalog points outside its release", { index_key: descriptor.index_key });
190
190
  }
191
191
  const key = `${config.prefix}${descriptor.index_key}`;
192
- const object = await getTosObject(config, key, { fetchImpl });
192
+ const object = await getOssObject(config, key, { fetchImpl });
193
193
  const actual = sha256(object.bytes);
194
194
  if (actual !== descriptor.sha256) throw releaseError("INDEX_UPLOAD_VERIFY_FAILED", "published index SHA-256 mismatch", { key, expected_sha256: descriptor.sha256, actual_sha256: actual });
195
195
  const index = parseJson(indexBytes(object, descriptor.index_key), "WIKI_SEARCH_INDEX_INVALID", key);
@@ -211,9 +211,9 @@ export async function verifyPublishedIndex({ env, fetchImpl = fetch, concurrency
211
211
  }
212
212
 
213
213
  export async function smokePublishedIndex({ env, fetchImpl = fetch, categoryPath, query, limit = 8 } = {}) {
214
- const config = createTosConfig(env);
214
+ const config = createOssConfig(env);
215
215
  const catalogKey = `${config.prefix}_catalog.json`;
216
- const catalogObject = await getTosObject(config, catalogKey, { fetchImpl });
216
+ const catalogObject = await getOssObject(config, catalogKey, { fetchImpl });
217
217
  const catalog = parseJson(catalogObject.bytes, "WIKI_SEARCH_CATALOG_INVALID", catalogKey);
218
218
  const requested = Array.isArray(categoryPath) ? categoryPath : typeof categoryPath === "string" ? categoryPath.split("/") : null;
219
219
  const descriptor = requested
@@ -221,7 +221,7 @@ export async function smokePublishedIndex({ env, fetchImpl = fetch, categoryPath
221
221
  : catalog.categories?.[0];
222
222
  if (!descriptor) throw releaseError("SMOKE_CATEGORY_NOT_FOUND", "no published category is available for smoke test", { category_path: requested });
223
223
  const indexKey = `${config.prefix}${descriptor.index_key}`;
224
- const indexObject = await getTosObject(config, indexKey, { fetchImpl });
224
+ const indexObject = await getOssObject(config, indexKey, { fetchImpl });
225
225
  const index = parseJson(indexBytes(indexObject, descriptor.index_key), "WIKI_SEARCH_INDEX_INVALID", indexKey);
226
226
  const probe = index.records?.[0];
227
227
  if (!probe) throw releaseError("SMOKE_EMPTY_CATEGORY", "published category has no records", { category_path: descriptor.category_path });
@@ -270,11 +270,11 @@ export async function publishHardwareSearchIndex({ sourceDir, outputDir, release
270
270
  catalog_sha256: local.catalog_sha256,
271
271
  };
272
272
 
273
- const config = createTosConfig(env);
273
+ const config = createOssConfig(env);
274
274
  if (built.generated_source) {
275
275
  const manifestBytes = fs.readFileSync(path.join(built.generated_source, "_manifest.json"));
276
276
  const entries = JSON.parse(manifestBytes);
277
- const limit = Number(concurrency || env?.TOS_PUBLISH_CONCURRENCY || 6);
277
+ const limit = Number(concurrency || env?.OSS_PUBLISH_CONCURRENCY || 6);
278
278
  await mapConcurrent(entries, Number.isFinite(limit) ? limit : 6, async entry => {
279
279
  const bytes = fs.readFileSync(safeFile(built.generated_source, entry.path));
280
280
  if (sha256(bytes) !== entry.sha256) throw releaseError("SOURCE_WIKI_MISMATCH", "generated Wiki changed after build");
@@ -285,28 +285,28 @@ export async function publishHardwareSearchIndex({ sourceDir, outputDir, release
285
285
  }
286
286
  let manifest = await requireMatchingManifest(config, local.catalog, fetchImpl, { optional: true });
287
287
  if (!manifest && !bootstrapManifest) {
288
- throw releaseError("SOURCE_MANIFEST_MISSING", "TOS Wiki Manifest is missing; rerun with --bootstrap-manifest to verify every remote Wiki object before creating it", {
288
+ throw releaseError("SOURCE_MANIFEST_MISSING", "OSS Wiki Manifest is missing; rerun with --bootstrap-manifest to verify every remote Wiki object before creating it", {
289
289
  key: `${config.prefix}${local.catalog.source_manifest.object_key || "_manifest.json"}`,
290
290
  });
291
291
  }
292
292
  if (!manifest) {
293
- manifest = await bootstrapRemoteManifest({ config, catalog: local.catalog, sourceDir, fetchImpl, concurrency: concurrency || env?.TOS_PUBLISH_CONCURRENCY });
293
+ manifest = await bootstrapRemoteManifest({ config, catalog: local.catalog, sourceDir, fetchImpl, concurrency: concurrency || env?.OSS_PUBLISH_CONCURRENCY });
294
294
  }
295
295
  const catalogKey = `${config.prefix}_catalog.json`;
296
- const previousCatalog = await getTosObject(config, catalogKey, { fetchImpl, optional: true });
297
- const limit = Number(concurrency || env?.TOS_PUBLISH_CONCURRENCY || 6);
296
+ const previousCatalog = await getOssObject(config, catalogKey, { fetchImpl, optional: true });
297
+ const limit = Number(concurrency || env?.OSS_PUBLISH_CONCURRENCY || 6);
298
298
  const uploaded = await mapConcurrent(local.files, Number.isFinite(limit) ? limit : 6, item =>
299
299
  putImmutable(config, `${config.prefix}${item.key}`, item.bytes, fetchImpl));
300
300
 
301
301
  // OSS 没有条件 PUT(见 putImmutable),激活前按内容 SHA 重读比对;OSS 对 JSON 对象不回 ETag,
302
302
  // 故不能比 ETag。重读到写入之间仍有窗口,写后 verify 的 release_id 比对兜住输掉的竞争。
303
303
  const previousSha256 = previousCatalog ? sha256(previousCatalog.bytes) : null;
304
- const currentCatalog = await getTosObject(config, catalogKey, { fetchImpl, optional: true });
304
+ const currentCatalog = await getOssObject(config, catalogKey, { fetchImpl, optional: true });
305
305
  const currentSha256 = currentCatalog ? sha256(currentCatalog.bytes) : null;
306
306
  if (currentSha256 !== previousSha256) {
307
307
  throw releaseError("CATALOG_ACTIVATION_RACE", "active catalog changed while this release was uploading", { expected_sha256: previousSha256, actual_sha256: currentSha256 });
308
308
  }
309
- await putTosObject(config, catalogKey, local.catalogBytes, { fetchImpl, headers: { "content-type": "application/json" } });
309
+ await putOssObject(config, catalogKey, local.catalogBytes, { fetchImpl, headers: { "content-type": "application/json" } });
310
310
  const verified = await verifyPublishedIndex({ env, fetchImpl, concurrency: limit });
311
311
  if (verified.release_id !== local.catalog.release_id) {
312
312
  throw releaseError("CATALOG_ACTIVATION_RACE", "active catalog is not the release just published", { expected_release_id: local.catalog.release_id, actual_release_id: verified.release_id });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: abelien-hardware-selection
3
- description: 为已澄清的需求分层选择硬件分类,经真实 TOS 召回、英文 Volc 搜索加来源直读的来源绑定补选(Prismer 默认关闭)、Agent 语义比较与本地 hardware.select 校验身份、数量和引用。用于硬件选型,不用于需求澄清、独立数据库兼容求解或实物验证。
3
+ description: 为已澄清的需求分层选择硬件分类,经真实 OSS 召回、英文 Volc 搜索加来源直读的来源绑定补选(Prismer 默认关闭)、Agent 语义比较与本地 hardware.select 校验身份、数量和引用。用于硬件选型,不用于需求澄清、独立数据库兼容求解或实物验证。
4
4
  license: MIT
5
5
  compatibility:
6
6
  - claude-code
@@ -14,7 +14,7 @@ compatibility:
14
14
 
15
15
  可用 `node <本包>/scripts/load-guide.mjs entry`,随后按需调用 `level1 --paths compute-control,power-energy`、`level2 --paths compute-control/controller-board,power-energy/charging`、`contract`。每次只返回指定文件及内容;分类不确定时再加载相邻页。
16
16
 
17
- 分类阶段只产出功能槽位、数量、三级路径及用户硬约束,不指定 SKU。当前本地链路由 `hardware.search` 读取真实 TOS 索引与 Wiki,Agent 逐项比较,再由 `hardware.select` 回读召回资源校验身份、数量和证据。无需独立服务地址;缺证据保持待确认。`verified` 是 Wiki 证据判断,不是独立数据库兼容求解或实物测试。旧规则快照中的数据库契约不是当前完成声明,运行时以 [本地选型契约](references/selection-service-contract.md) 为准。
17
+ 分类阶段只产出功能槽位、数量、三级路径及用户硬约束,不指定 SKU。当前本地链路由 `hardware.search` 读取真实 OSS 索引与 Wiki,Agent 逐项比较,再由 `hardware.select` 回读召回资源校验身份、数量和证据。无需独立服务地址;缺证据保持待确认。`verified` 是 Wiki 证据判断,不是独立数据库兼容求解或实物测试。旧规则快照中的数据库契约不是当前完成声明,运行时以 [本地选型契约](references/selection-service-contract.md) 为准。
18
18
 
19
19
  ## 输入形状
20
20
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "abelien-hardware-selection",
3
- "version": "0.7.0",
4
- "description": "本地硬件选型:真实 TOS 召回、Agent 证据判断与受信资源校验;不是数据库兼容性求解器",
3
+ "version": "0.7.1",
4
+ "description": "本地硬件选型:真实 OSS 召回、Agent 证据判断与受信资源校验;不是数据库兼容性求解器",
5
5
  "resources": ["hardware-selection"],
6
6
  "requires": ["abelien-harness", "abelien-hardware"],
7
7
  "capabilities": {
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "references", "hardware-selection-v2");
7
7
  const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
- const runtimeBoundary = "分类快照只用于路由;其中数据库服务的描述不是当前实现。当前执行契约请加载 contract:TOS 真实召回、Agent 证据比较、本地 hardware.select;不是独立数据库兼容求解或实物测试。";
8
+ const runtimeBoundary = "分类快照只用于路由;其中数据库服务的描述不是当前实现。当前执行契约请加载 contract:OSS 真实召回、Agent 证据比较、本地 hardware.select;不是独立数据库兼容求解或实物测试。";
9
9
 
10
10
  export function loadGuide(phase, paths = []) {
11
11
  let files;
@@ -78,7 +78,7 @@ adapter 返回值整体作为新资源 body:
78
78
  ```json
79
79
  {
80
80
  "type": "module",
81
- "path": "scripts/tos-wiki-search.mjs",
81
+ "path": "scripts/oss-wiki-search.mjs",
82
82
  "export": "searchHardware"
83
83
  }
84
84
  ```
@@ -6,7 +6,7 @@
6
6
 
7
7
  本仓只带创建谱系的 **wire 契约**与受信客户端(`ab studio creation create|active|get`,宿主中立说明见 AGENTS.md 与 workflow 规范),不带第二份编排:Web 侧的创建请求只接受 schemaVersion、creationOperationId、inputRevision(必须 0)、manifestDigest、executionDefinitionVersion、executionDefinitionDigest、executionPath,并要求 studio.create 作用域;active/get 要求 state:read。HTTP origin/token 固定来自受信宿主 0600 context,不可由模型传入。工具登记 request/response 恰好一个 creationOperationId 或 modificationOperationId。creation receipt 与 Web 的 revision-zero 契约一致:requirementsSnapshot、requirementsHash、三项来源 Ref、plannedOccurrenceBindings,baseline.designRevision 为 0、designDigest 为 null;receipt 只保存、回读和观察,不自动 advance。
8
8
 
9
- hardware.search 只检索已配置的 TOS Wiki 压缩分类索引(索引缺失即 fail-closed,不静默回落全量扫描);hardware.finalize 只把本次召回的候选比较归一化为 hardware-proposal(proposed 或 needs_confirmation),**不产生正式选型、也不写 verified**;但每个 selected_models 条目仍必须给出显式的 1–128 整数 quantity——不授正式权限不等于放弃可复核性,缺了数量,覆盖账本上的 verified_allocated_quantity 就没有对照物。正式选型由独立包 `abelien-hardware-selection` 的 hardware.select 提供——**本地 module adapter**(`scripts/select.mjs`),不再读 `ABELIEN_HARDWARE_SELECT_URL`,也不会因为设了旧 URL 就静默切回 HTTP;该包运行时要 import `abelien-hardware`(`domain.json` 的 `requires` 声明),域包在安装清单中一并落盘,须在干净隔离安装目录验证。hardware.revise 基于已验证选型做局部修订(HTTP,`env:ABELIEN_HARDWARE_REVISE_URL`)。
9
+ hardware.search 只检索已配置的 OSS Wiki 压缩分类索引(索引缺失即 fail-closed,不静默回落全量扫描);hardware.finalize 只把本次召回的候选比较归一化为 hardware-proposal(proposed 或 needs_confirmation),**不产生正式选型、也不写 verified**;但每个 selected_models 条目仍必须给出显式的 1–128 整数 quantity——不授正式权限不等于放弃可复核性,缺了数量,覆盖账本上的 verified_allocated_quantity 就没有对照物。正式选型由独立包 `abelien-hardware-selection` 的 hardware.select 提供——**本地 module adapter**(`scripts/select.mjs`),不再读 `ABELIEN_HARDWARE_SELECT_URL`,也不会因为设了旧 URL 就静默切回 HTTP;该包运行时要 import `abelien-hardware`(`domain.json` 的 `requires` 声明),域包在安装清单中一并落盘,须在干净隔离安装目录验证。hardware.revise 基于已验证选型做局部修订(HTTP,`env:ABELIEN_HARDWARE_REVISE_URL`)。
10
10
 
11
11
  ## 固定与验证
12
12
 
@@ -18,4 +18,4 @@ prepack 的 clarify 锚点检查在缺少原始上游仓库时会报告 skipped
18
18
 
19
19
  ## 明确付费边界
20
20
 
21
- 构建、离线安装、fixture 测试、doctor、capability list、hardware.finalize 和 receipt 本地消费不调用 provider。真实 TOS search 是网络读取,可能有存储流量成本;本次也不执行。studio creation/tool register 会写受信 Web 状态,invocation submit 可能触发平台付费生成;generate 3d submit 的 confirm-spend 也是显式花费边界。此构建不调用任何这些真实入口,不设置账号/价格,不继承旧 50 元授权。集成验收不能以本包单测替代真实 Web/Agent 消费、同方案产物和反馈闭环验收。
21
+ 构建、离线安装、fixture 测试、doctor、capability list、hardware.finalize 和 receipt 本地消费不调用 provider。真实 OSS search 是网络读取,可能有存储流量成本;本次也不执行。studio creation/tool register 会写受信 Web 状态,invocation submit 可能触发平台付费生成;generate 3d submit 的 confirm-spend 也是显式花费边界。此构建不调用任何这些真实入口,不设置账号/价格,不继承旧 50 元授权。集成验收不能以本包单测替代真实 Web/Agent 消费、同方案产物和反馈闭环验收。
@@ -167,7 +167,14 @@ export function createStore(workdir) {
167
167
  }
168
168
 
169
169
  // ---------- CLI shell ----------
170
- export const VERSION = "0.2.3";
170
+ // doctor must report the shipped package version, never a frozen constant (0.2.3 drifted across
171
+ // six releases). The npm tree always ships package.json at the skill root; standalone skill copies
172
+ // fall back to FALLBACK_VERSION, which scripts/release-check.mjs pins to package.json.
173
+ const FALLBACK_VERSION = "0.8.0";
174
+ export let VERSION = FALLBACK_VERSION;
175
+ try {
176
+ VERSION = String(JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || FALLBACK_VERSION);
177
+ } catch {}
171
178
 
172
179
  const USAGE_TEXT = `ab — Abelien harness CLI
173
180
 
@@ -484,6 +484,18 @@ export async function studioCmd(pos, flags, deps = {}) {
484
484
  const suffix = action === "active" ? "active" : text(pos[2], "creationOperationId", UUID, 36);
485
485
  return request(context, "GET", `creations/${suffix}`, undefined, "creation", fetchImpl);
486
486
  }
487
+ if (resource === "modification" && (action === "active" || action === "get")) {
488
+ argsExactly(pos, action === "active" ? 2 : 3, "studio modification active | get <id>"); flagsOnly(flags);
489
+ requireScope(context, "state:read");
490
+ const suffix = action === "active" ? "active" : text(pos[2], "modificationOperationId", UUID, 36);
491
+ return request(context, "GET", `modifications/${suffix}`, undefined, "modification", fetchImpl);
492
+ }
493
+ if (resource === "invocation" && action === "get") {
494
+ argsExactly(pos, 3, "studio invocation get <invocationId>"); flagsOnly(flags);
495
+ requireScope(context, "state:read");
496
+ const id = text(pos[2], "invocationId", UUID, 36);
497
+ return request(context, "GET", `invocations/${id}`, undefined, "invocation", fetchImpl);
498
+ }
487
499
  requireScope(context, "tools:invoke");
488
500
  if (resource === "creation") {
489
501
  if (action === "create") {
@@ -495,18 +507,12 @@ export async function studioCmd(pos, flags, deps = {}) {
495
507
  }
496
508
  if (resource === "modification" && action === "create") {
497
509
  argsExactly(pos, 2, "studio modification create --input '<json>'"); flagsOnly(flags, ["input"]);
510
+ // The broker settles modification writes on studio.modify; the actionScope vocabulary stays
511
+ // Web-owned (component_replace / parameter_patch both carry it), so only the scope is mirrored.
512
+ requireScope(context, "studio.modify");
498
513
  const input = validateModificationRequest(parseJsonFlag(flags.input));
499
514
  return request(context, "POST", "modifications", input, "modification", fetchImpl);
500
515
  }
501
- if (resource === "modification" && action === "active") {
502
- argsExactly(pos, 2, "studio modification active"); flagsOnly(flags);
503
- return request(context, "GET", "modifications/active", undefined, "modification", fetchImpl);
504
- }
505
- if (resource === "modification" && action === "get") {
506
- argsExactly(pos, 3, "studio modification get <modificationOperationId>"); flagsOnly(flags);
507
- const id = text(pos[2], "modificationOperationId", UUID, 36);
508
- return request(context, "GET", `modifications/${id}`, undefined, "modification", fetchImpl);
509
- }
510
516
  if (resource === "tool" && action === "register") {
511
517
  argsExactly(pos, 2, "studio tool register --input '<json>'"); flagsOnly(flags, ["input"]);
512
518
  const input = validateToolOperationRequest(parseJsonFlag(flags.input));
@@ -517,10 +523,5 @@ export async function studioCmd(pos, flags, deps = {}) {
517
523
  const input = validateInvocationRequest(parseJsonFlag(flags.input));
518
524
  return request(context, "POST", "invocations", input, "invocation", fetchImpl);
519
525
  }
520
- if (resource === "invocation" && action === "get") {
521
- argsExactly(pos, 3, "studio invocation get <invocationId>"); flagsOnly(flags);
522
- const id = text(pos[2], "invocationId", UUID, 36);
523
- return request(context, "GET", `invocations/${id}`, undefined, "invocation", fetchImpl);
524
- }
525
526
  throw fail("USAGE", "studio command is probe, replacement request, creation|modification create|active|get, tool register, or invocation submit|get");
526
527
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-reference",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "参考研究域包:按 Agent 的检索计划收集产品识别、拆解资料与相似产品证据(检索、公开页面、主图、用户原图测量),再核验 Agent 的判断,产出 reference-evidence v1;判定码来自确定性检查",
5
5
  "requires": [
6
6
  "abelien-harness"
@@ -62,6 +62,7 @@
62
62
  ### 核验
63
63
 
64
64
  - 引用经过 NFKC(全半角)与空白归一后,必须逐字出现在该页已保存的正文里,且归一后至少 8 个字。只能引用读取成功的页面。
65
+ - 判定产出的引文对象保留该页的 `text_source` 标记:页面正文读不到、这段引用实际取自检索结果摘要时,`quote.text_source` 为 `search_snippet`。报告消费者据此即可区分「页面原文」与「检索摘要」,无需回读证据资源。
65
66
  - 候选的 `quote.page_id` 必须等于它的 `item_id`;没读到的页面(`too_large`、不可用)不能作为候选,因而不会被判为 `closest`。`name` 只有出现在该引用里(`name_in_quote: true`)才替换格子标题,否则标题保持页面标题,依据与冲突说明也用页面标题。
66
67
  - 给出尺寸时,宽、高(以及给出时的深)都必须以带长度单位的数值出现在同一条引用里:单位为 mm/毫米、cm/厘米、m/米、in/inch/英寸,换算成毫米后与提交值一致(容差 0.2% 或 0.05 mm 取大);`118 × 118 × 215 mm` 这类连写时,单位作用于整串数值。重量、价格等不带长度单位的数字不算。没给尺寸时,比例核对为 `unknown`(`no_dimensions`),并写入 `unknowns`。
67
68
  - 规格值(`specs[].value`)必须能在其引用中定位:归一后是引用的子串;或值里的每个数字都以相同单位出现在引用里(长度按毫米换算,值里没写单位的数字只要求数字在引用里)。定位不到时整份判断以 `VALIDATION` 拒绝,不会把 Agent 的说法写成来源事实。
@@ -159,7 +159,7 @@
159
159
  "properties": {
160
160
  "name_in_quote": { "type": "boolean", "description": "名称是否出现在该候选自己的引用里;只有为 true 时名称才替换画布格子的标题,否则标题保持页面标题" },
161
161
  "dimensions_mm": { "description": "页面未给尺寸时为 null,此时比例核对为 unknown;给出时每个值都以带长度单位(mm/cm/m/in 等,换算到毫米一致)的数值出现在同一引用里", "oneOf": [{ "type": "null" }, { "type": "object", "required": ["width", "height"] }] },
162
- "quote": { "type": "object", "required": ["page_id", "text"], "description": "page_id 必须等于该候选的 item_id;text 规范化后至少 8 字,逐字出现在该页正文里" },
162
+ "quote": { "type": "object", "required": ["page_id", "text"], "description": "page_id 必须等于该候选的 item_id;text 规范化后至少 8 字,逐字出现在该页正文里", "properties": { "page_id": { "type": "string" }, "text": { "type": "string" }, "text_source": { "enum": ["search_snippet"], "description": "仅当该页正文读不到、这段引用实际取自检索结果摘要时出现(同 page 的 text_source)" } } },
163
163
  "ratio_check": { "type": "object", "required": ["status"], "properties": { "status": { "enum": ["consistent", "mismatch", "unknown"] } } }
164
164
  }
165
165
  },
@@ -75,7 +75,9 @@ export async function finalizeReference({ input, readResource, readBlob }) {
75
75
  const quoted = words(quote.text, `${label}.text`, 500);
76
76
  if ([...normalize(quoted)].length < QUOTE_MIN_CHARS) invalid(`${label}.text must be a quote of at least ${QUOTE_MIN_CHARS} characters`);
77
77
  if (!pageText.get(page.id).includes(normalize(quoted))) invalid(`${label}.text must appear verbatim in page ${page.id}`);
78
- return { page_id: page.id, text: quoted };
78
+ // The finalized quote carries the page's own text_source mark: a judgement quoted from the search
79
+ // summary (search_snippet) stays distinguishable at the report layer from one quoted from page body.
80
+ return { page_id: page.id, text: quoted, ...(page.text_source === undefined ? {} : { text_source: page.text_source }) };
79
81
  };
80
82
  const itemOf = (id, kinds, label) => {
81
83
  const item = items.get(id);
@@ -50,7 +50,7 @@ node <skills目录>/abelien-harness/scripts/ab.mjs --workdir <作品工作目录
50
50
  --input '{"intent":"restore","artifacts":{"reference_final":true}}'
51
51
  ```
52
52
 
53
- 它是纯计算,返回下一阶段、目标与 `directives`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后。restore 默认锁定外形;锁定时,CAD 阶段的规则会写明不得放大外壳;未锁定时只允许在 appearance 认可的外观容差内调整外壳,超出容差换件或交用户取舍。意图的划分见 [意图路由](../../docs/architecture/intent-routing.md)。阶段计划是[完整用户旅程](references/user-journey.md)中 A1 通过之后到交付首版这一段(及其旁路)的机读派生,规则正文以 user-journey.md 为准;本能力在[能力映射](references/harness-binding.md#当前能力映射)中登记。
53
+ 它是纯计算,返回下一阶段、目标、`directives` 与 `actions`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后;`actions` 是该阶段允许的动作(取自计划的 `action_catalog`),宿主把它们映射成自己的工具,只放行这些。restore 默认锁定外形;锁定时,CAD 阶段的规则会写明不得放大外壳;未锁定时只允许在 appearance 认可的外观容差内调整外壳,超出容差换件或交用户取舍。意图的划分见 [意图路由](../../docs/architecture/intent-routing.md)。阶段计划是[完整用户旅程](references/user-journey.md)中 A1 通过之后到交付首版这一段(及其旁路)的机读派生,规则正文以 user-journey.md 为准;本能力在[能力映射](references/harness-binding.md#当前能力映射)中登记。
54
54
 
55
55
  本包定义作品事实之间的关系,不表示已实现完整的产品后台。现有可用能力和未接入项集中维护在[能力映射](references/harness-binding.md),不使用旧 `workspace.*` mock 顶替。
56
56