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.
- package/.env.example +19 -20
- package/README.md +12 -12
- package/package.json +1 -1
- package/skills/abelien-hardware/SKILL.md +4 -4
- package/skills/abelien-hardware/domain.json +4 -4
- package/skills/abelien-hardware/references/knowledge-recall-service-contract.md +4 -4
- package/skills/abelien-hardware/references/search-index-format.md +5 -5
- package/skills/abelien-hardware/references/search-index-release.md +7 -7
- package/skills/abelien-hardware/references/selection-service-contract.md +3 -3
- package/skills/abelien-hardware/scripts/build-search-index.mjs +1 -1
- package/skills/abelien-hardware/scripts/oss-object-store.mjs +247 -0
- package/skills/abelien-hardware/scripts/{tos-wiki-search.mjs → oss-wiki-search.mjs} +19 -19
- package/skills/abelien-hardware/scripts/search-index-release.mjs +25 -25
- package/skills/abelien-hardware-selection/SKILL.md +2 -2
- package/skills/abelien-hardware-selection/domain.json +2 -2
- package/skills/abelien-hardware-selection/scripts/load-guide.mjs +1 -1
- package/skills/abelien-harness/references/adapter-contract.md +1 -1
- package/skills/abelien-harness/references/m1-integration.md +2 -2
- package/skills/abelien-harness/scripts/ab.mjs +8 -1
- package/skills/abelien-harness/scripts/studio.mjs +15 -14
- package/skills/abelien-reference/domain.json +1 -1
- package/skills/abelien-reference/references/research-contract.md +1 -0
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +1 -1
- package/skills/abelien-reference/scripts/finalize.mjs +3 -1
- package/skills/abelien-workspace/SKILL.md +1 -1
- package/skills/abelien-workspace/assets/journey-stage-plan.json +46 -13
- package/skills/abelien-workspace/domain.json +3 -2
- package/skills/abelien-workspace/references/hardware-journey-orchestration.md +2 -2
- package/skills/abelien-workspace/references/harness-binding.md +3 -3
- package/skills/abelien-workspace/references/user-journey.md +5 -5
- package/skills/abelien-workspace/scripts/journey-next.mjs +10 -2
- package/skills/abelien-workspace/workflows/user-journey.json +1 -1
- 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 {
|
|
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
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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, "
|
|
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: `
|
|
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", "
|
|
395
|
-
if (!Array.isArray(manifest)) throw error("WIKI_MANIFEST_INVALID", "
|
|
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, "
|
|
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: `
|
|
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 =
|
|
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, "
|
|
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
|
|
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.
|
|
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", "
|
|
445
|
-
key: `${config.prefix}_catalog.json`, migration_env: "
|
|
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 "./
|
|
7
|
-
import {
|
|
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
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
178
|
+
const config = createOssConfig(env);
|
|
179
179
|
const catalogKey = `${config.prefix}_catalog.json`;
|
|
180
|
-
const catalogObject = await
|
|
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?.
|
|
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
|
|
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 =
|
|
214
|
+
const config = createOssConfig(env);
|
|
215
215
|
const catalogKey = `${config.prefix}_catalog.json`;
|
|
216
|
-
const catalogObject = await
|
|
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
|
|
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 =
|
|
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?.
|
|
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", "
|
|
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?.
|
|
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
|
|
297
|
-
const limit = Number(concurrency || env?.
|
|
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
|
|
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
|
|
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: 为已澄清的需求分层选择硬件分类,经真实
|
|
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` 读取真实
|
|
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.
|
|
4
|
-
"description": "本地硬件选型:真实
|
|
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:
|
|
8
|
+
const runtimeBoundary = "分类快照只用于路由;其中数据库服务的描述不是当前实现。当前执行契约请加载 contract:OSS 真实召回、Agent 证据比较、本地 hardware.select;不是独立数据库兼容求解或实物测试。";
|
|
9
9
|
|
|
10
10
|
export function loadGuide(phase, paths = []) {
|
|
11
11
|
let files;
|
|
@@ -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 只检索已配置的
|
|
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。真实
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|